Package org.twodividedbyzero.idea.findbugs.gui.common

Source Code of org.twodividedbyzero.idea.findbugs.gui.common.MultiSplitLayout$Split

/*
* Copyright 2008-2013 Andre Pfeiler
*
* This file is part of FindBugs-IDEA.
*
* FindBugs-IDEA is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FindBugs-IDEA 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FindBugs-IDEA.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.twodividedbyzero.idea.findbugs.gui.common;

import javax.swing.UIManager;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;


/**
* The MultiSplitLayout layout manager recursively arranges its
* components in row and column groups called "Splits".  Elements of
* the layout are separated by gaps called "Dividers".  The overall
* layout is defined with a simple tree model whose nodes are
* instances of MultiSplitLayout.Split, MultiSplitLayout.Divider,
* and MultiSplitLayout.Leaf. Named Leaf nodes represent the space
* allocated to a component that was added with a constraint that
* matches the Leaf's name.  Extra space is distributed
* among row/column siblings according to their 0.0 to 1.0 weight.
* If no weights are specified then the last sibling always gets
* all of the extra space, or space reduction.
* <p/>
* <p/>
* Although MultiSplitLayout can be used with any Container, it's
* the default layout manager for MultiSplitPane.  MultiSplitPane
* supports interactively dragging the Dividers, accessibility,
* and other features associated with split panes.
* <p/>
* <p/>
* All properties in this class are bound: when a properties value
* is changed, all PropertyChangeListeners are fired.
*
* @author Hans Muller
* @author Andre Pfeiler - some minor adaptations
* @see MultiSplitPane
*/

@SuppressWarnings({"HardCodedStringLiteral", "NestedAssignment"})
public class MultiSplitLayout implements LayoutManager {

  private final Map<String, Component> childMap = new HashMap<String, Component>();
  private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
  private transient Node model;
  private int dividerSize;
  private boolean floatingDividers = true;


  /**
   * Create a MultiSplitLayout with a default model with a single
   * Leaf node named "default".
   * <p/>
   * #see setModel
   */
  public MultiSplitLayout() {
    this(new Leaf("default"));
  }


  /**
   * Create a MultiSplitLayout with the specified model.
   * <p/>
   * #see setModel
   * @param model
   */
  private MultiSplitLayout(final Node model) {
    this.model = model;
    dividerSize = UIManager.getInt("SplitPane.dividerSize");
    if (dividerSize == 0) {
      dividerSize = 7;
    }
  }


  public void addPropertyChangeListener(final PropertyChangeListener listener) {
    if (listener != null) {
      pcs.addPropertyChangeListener(listener);
    }
  }


  public void removePropertyChangeListener(final PropertyChangeListener listener) {
    if (listener != null) {
      pcs.removePropertyChangeListener(listener);
    }
  }


  public PropertyChangeListener[] getPropertyChangeListeners() {
    return pcs.getPropertyChangeListeners();
  }


  private void firePCS(final String propertyName, final Object oldValue, final Object newValue) {
    if (!(oldValue != null && newValue != null && oldValue.equals(newValue))) {
      pcs.firePropertyChange(propertyName, oldValue, newValue);
    }
  }


  /**
   * Return the root of the tree of Split, Leaf, and Divider nodes
   * that define this layout.
   *
   * @return the value of the model property
   * @see #setModel
   */
  Node getModel() {
    return model;
  }


  /**
   * Set the root of the tree of Split, Leaf, and Divider nodes
   * that define this layout.  The model can be a Split node
   * (the typical case) or a Leaf.  The default value of this
   * property is a Leaf named "default".
   *
   * @param model the root of the tree of Split, Leaf, and Divider node
   * @throws IllegalArgumentException if model is a Divider or null
   * @see #getModel
   */
  public void setModel(final Node model) {
    if (model == null || model instanceof Divider) {
      throw new IllegalArgumentException("invalid model");
    }
    this.model = model;
    firePCS("model", model, model);
  }


  /**
   * Returns the width of Dividers in Split rows, and the height of
   * Dividers in Split columns.
   *
   * @return the value of the dividerSize property
   * @see #setDividerSize
   */
  int getDividerSize() {
    return dividerSize;
  }


  /**
   * Sets the width of Dividers in Split rows, and the height of
   * Dividers in Split columns.  The default value of this property
   * is the same as for JSplitPane Dividers.
   *
   * @param dividerSize the size of dividers (pixels)
   * @throws IllegalArgumentException if dividerSize < 0
   * @see #getDividerSize
   */
  public void setDividerSize(final int dividerSize) {
    if (dividerSize < 0) {
      throw new IllegalArgumentException("invalid dividerSize");
    }
    final int oldDividerSize = this.dividerSize;
    this.dividerSize = dividerSize;
    firePCS("dividerSize", oldDividerSize, dividerSize);
  }


  /**
   * @return the value of the floatingDividers property
   * @see #setFloatingDividers
   */
  public boolean getFloatingDividers() {
    return floatingDividers;
  }


  /**
   * If true, Leaf node bounds match the corresponding component's
   * preferred size and Splits/Dividers are resized accordingly.
   * If false then the Dividers define the bounds of the adjacent
   * Split and Leaf nodes.  Typically this property is set to false
   * after the (MultiSplitPane) user has dragged a Divider.
   *
   * @param floatingDividers
   * @see #getFloatingDividers
   */
  public void setFloatingDividers(final boolean floatingDividers) {
    final boolean oldFloatingDividers = this.floatingDividers;
    this.floatingDividers = floatingDividers;
    firePCS("floatingDividers", oldFloatingDividers, floatingDividers);
  }


  /**
   * Add a component to this MultiSplitLayout.  The
   * <code>name</code> should match the name property of the Leaf
   * node that represents the bounds of <code>child</code>.  After
   * layoutContainer() recomputes the bounds of all of the nodes in
   * the model, it will set this child's bounds to the bounds of the
   * Leaf node with <code>name</code>.  Note: if a component was already
   * added with the same name, this method does not remove it from
   * its parent.
   *
   * @param name  identifies the Leaf node that defines the child's bounds
   * @param child the component to be added
   * @see #removeLayoutComponent
   */
  public void addLayoutComponent(final String name, final Component child) {
    if (name == null) {
      throw new IllegalArgumentException("name not specified");
    }
    childMap.put(name, child);
  }


  /**
   * Removes the specified component from the layout.
   *
   * @param child the component to be removed
   * @see #addLayoutComponent
   */
  public void removeLayoutComponent(final Component child) {
    final String name = child.getName();
    if (name != null) {
      childMap.remove(name);
    }
  }


  private Component childForNode(final Node node) {
    if (node instanceof Leaf) {
      final Leaf leaf = (Leaf) node;
      final String name = leaf.getName();
      return name != null ? childMap.get(name) : null;
    }
    return null;
  }


  private Dimension preferredComponentSize(final Node node) {
    final Component child = childForNode(node);
    return child != null ? child.getPreferredSize() : new Dimension(0, 0);

  }


  private Dimension preferredNodeSize(final Node root) {
    if (root instanceof Leaf) {
      return preferredComponentSize(root);
    } else if (root instanceof Divider) {
      final int dividerSize = getDividerSize();
      return new Dimension(dividerSize, dividerSize);
    } else {
      final Split split = (Split) root;
      final List<Node> splitChildren = split.getChildren();
      int width = 0;
      int height = 0;
      if (split.isRowLayout()) {
        for (final Node splitChild : splitChildren) {
          final Dimension size = preferredNodeSize(splitChild);
          width += size.width;
          height = Math.max(height, size.height);
        }
      } else {
        for (final Node splitChild : splitChildren) {
          final Dimension size = preferredNodeSize(splitChild);
          width = Math.max(width, size.width);
          height += size.height;
        }
      }
      return new Dimension(width, height);
    }
  }


  private Dimension minimumNodeSize(final Node root) {
    if (root instanceof Leaf) {
      final Component child = childForNode(root);
      return child != null ? child.getMinimumSize() : new Dimension(0, 0);
    } else if (root instanceof Divider) {
      final int dividerSize = getDividerSize();
      return new Dimension(dividerSize, dividerSize);
    } else {
      final Split split = (Split) root;
      final List<Node> splitChildren = split.getChildren();
      int width = 0;
      int height = 0;
      if (split.isRowLayout()) {
        for (final Node splitChild : splitChildren) {
          final Dimension size = minimumNodeSize(splitChild);
          width += size.width;
          height = Math.max(height, size.height);
        }
      } else {
        for (final Node splitChild : splitChildren) {
          final Dimension size = minimumNodeSize(splitChild);
          width = Math.max(width, size.width);
          height += size.height;
        }
      }
      return new Dimension(width, height);
    }
  }


  private static Dimension sizeWithInsets(final Container parent, final Dimension size) {
    final Insets insets = parent.getInsets();
    final int width = size.width + insets.left + insets.right;
    final int height = size.height + insets.top + insets.bottom;
    return new Dimension(width, height);
  }


  public Dimension preferredLayoutSize(final Container parent) {
    final Dimension size = preferredNodeSize(getModel());
    return sizeWithInsets(parent, size);
  }


  public Dimension minimumLayoutSize(final Container parent) {
    final Dimension size = minimumNodeSize(getModel());
    return sizeWithInsets(parent, size);
  }


  private static Rectangle boundsWithYandHeight(final Rectangle bounds, final double y, final double height) {
    final Rectangle r = new Rectangle();
    r.setBounds((int) bounds.getX(), (int) y, (int) bounds.getWidth(), (int) height);
    return r;
  }


  private static Rectangle boundsWithXandWidth(final Rectangle bounds, final double x, final double width) {
    final Rectangle r = new Rectangle();
    r.setBounds((int) x, (int) bounds.getY(), (int) width, (int) bounds.getHeight());
    return r;
  }


  private static void minimizeSplitBounds(final Split split, final Rectangle bounds) {
    final Rectangle splitBounds = new Rectangle(bounds.x, bounds.y, 0, 0);
    final List<Node> splitChildren = split.getChildren();
    final Node lastChild = splitChildren.get(splitChildren.size() - 1);
    final Rectangle lastChildBounds = lastChild.getBounds();
    if (split.isRowLayout()) {
      final int lastChildMaxX = lastChildBounds.x + lastChildBounds.width;
      splitBounds.add(lastChildMaxX, bounds.y + bounds.height);
    } else {
      final int lastChildMaxY = lastChildBounds.y + lastChildBounds.height;
      splitBounds.add(bounds.x + bounds.width, lastChildMaxY);
    }
    split.setBounds(splitBounds);
  }


  private void layoutShrink(final Split split, final Rectangle bounds) {
    final Rectangle splitBounds = split.getBounds();
    final ListIterator<Node> splitChildren = split.getChildren().listIterator();
    //final Node lastWeightedChild = split.lastWeightedChild();

    if (split.isRowLayout()) {
      int totalWidth = 0;      // sum of the children's widths
      int minWeightedWidth = 0// sum of the weighted childrens' min widths
      int totalWeightedWidth = 0// sum of the weighted childrens' widths
      for (final Node splitChild : split.getChildren()) {
        final int nodeWidth = splitChild.getBounds().width;
        final int nodeMinWidth = Math.min(nodeWidth, minimumNodeSize(splitChild).width);
        totalWidth += nodeWidth;
        if (splitChild.getWeight() > 0.0) {
          minWeightedWidth += nodeMinWidth;
          totalWeightedWidth += nodeWidth;
        }
      }

      double x = bounds.getX();
      final double extraWidth = splitBounds.getWidth() - bounds.getWidth();
      double availableWidth = extraWidth;
      final boolean onlyShrinkWeightedComponents = totalWeightedWidth - minWeightedWidth > extraWidth;

      while (splitChildren.hasNext()) {
        final Node splitChild = splitChildren.next();
        final Rectangle splitChildBounds = splitChild.getBounds();
        final double minSplitChildWidth = minimumNodeSize(splitChild).getWidth();
        final double splitChildWeight = onlyShrinkWeightedComponents ? splitChild.getWeight() : splitChildBounds.getWidth() / (double) totalWidth;

        if (!splitChildren.hasNext()) {
          final double newWidth = Math.max(minSplitChildWidth, bounds.getMaxX() - x);
          final Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
          layout2(splitChild, newSplitChildBounds);
        } else if (availableWidth > 0.0 && splitChildWeight > 0.0) {
          final double allocatedWidth = Math.rint(splitChildWeight * extraWidth);
          final double oldWidth = splitChildBounds.getWidth();
          final double newWidth = Math.max(minSplitChildWidth, oldWidth - allocatedWidth);
          final Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
          layout2(splitChild, newSplitChildBounds);
          availableWidth -= oldWidth - splitChild.getBounds().getWidth();
        } else {
          final double existingWidth = splitChildBounds.getWidth();
          final Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
          layout2(splitChild, newSplitChildBounds);
        }
        x = splitChild.getBounds().getMaxX();
      }
    } else {
      int totalHeight = 0;      // sum of the children's heights
      int minWeightedHeight = 0// sum of the weighted childrens' min heights
      int totalWeightedHeight = 0// sum of the weighted childrens' heights
      for (final Node splitChild : split.getChildren()) {
        final int nodeHeight = splitChild.getBounds().height;
        final int nodeMinHeight = Math.min(nodeHeight, minimumNodeSize(splitChild).height);
        totalHeight += nodeHeight;
        if (splitChild.getWeight() > 0.0) {
          minWeightedHeight += nodeMinHeight;
          totalWeightedHeight += nodeHeight;
        }
      }

      double y = bounds.getY();
      final double extraHeight = splitBounds.getHeight() - bounds.getHeight();
      double availableHeight = extraHeight;
      final boolean onlyShrinkWeightedComponents = totalWeightedHeight - minWeightedHeight > extraHeight;

      while (splitChildren.hasNext()) {
        final Node splitChild = splitChildren.next();
        final Rectangle splitChildBounds = splitChild.getBounds();
        final double minSplitChildHeight = minimumNodeSize(splitChild).getHeight();
        final double splitChildWeight = onlyShrinkWeightedComponents ? splitChild.getWeight() : splitChildBounds.getHeight() / (double) totalHeight;

        if (!splitChildren.hasNext()) {
          final double oldHeight = splitChildBounds.getHeight();
          final double newHeight = Math.max(minSplitChildHeight, bounds.getMaxY() - y);
          final Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
          layout2(splitChild, newSplitChildBounds);
          availableHeight -= oldHeight - splitChild.getBounds().getHeight();
        } else if (availableHeight > 0.0 && splitChildWeight > 0.0) {
          final double allocatedHeight = Math.rint(splitChildWeight * extraHeight);
          final double oldHeight = splitChildBounds.getHeight();
          final double newHeight = Math.max(minSplitChildHeight, oldHeight - allocatedHeight);
          final Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
          layout2(splitChild, newSplitChildBounds);
          availableHeight -= oldHeight - splitChild.getBounds().getHeight();
        } else {
          final double existingHeight = splitChildBounds.getHeight();
          final Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
          layout2(splitChild, newSplitChildBounds);
        }
        y = splitChild.getBounds().getMaxY();
      }
    }

    /* The bounds of the Split node root are set to be
     * big enough to contain all of its children. Since
     * Leaf children can't be reduced below their
     * (corresponding java.awt.Component) minimum sizes,
     * the size of the Split's bounds maybe be larger than
     * the bounds we were asked to fit within.
     */
    minimizeSplitBounds(split, bounds);
  }


  private void layoutGrow(final Split split, final Rectangle bounds) {
    final Rectangle splitBounds = split.getBounds();
    final ListIterator<Node> splitChildren = split.getChildren().listIterator();
    final Node lastWeightedChild = split.lastWeightedChild();

    /* Layout the Split's child Nodes' along the X axis.  The bounds
       * of each child will have the same y coordinate and height as the
       * layoutGrow() bounds argument.  Extra width is allocated to the
       * to each child with a non-zero weight:
       *     newWidth = currentWidth + (extraWidth * splitChild.getWeight())
       * Any extraWidth "left over" (that's availableWidth in the loop
       * below) is given to the last child.  Note that Dividers always
       * have a weight of zero, and they're never the last child.
       */
    if (split.isRowLayout()) {
      double x = bounds.getX();
      final double extraWidth = bounds.getWidth() - splitBounds.getWidth();
      double availableWidth = extraWidth;

      while (splitChildren.hasNext()) {
        final Node splitChild = splitChildren.next();
        final Rectangle splitChildBounds = splitChild.getBounds();
        final double splitChildWeight = splitChild.getWeight();

        if (!splitChildren.hasNext()) {
          final double newWidth = bounds.getMaxX() - x;
          final Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
          layout2(splitChild, newSplitChildBounds);
        } else if (availableWidth > 0.0 && splitChildWeight > 0.0) {
          final double allocatedWidth = splitChild.equals(lastWeightedChild) ? availableWidth : Math.rint(splitChildWeight * extraWidth);
          final double newWidth = splitChildBounds.getWidth() + allocatedWidth;
          final Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, newWidth);
          layout2(splitChild, newSplitChildBounds);
          availableWidth -= allocatedWidth;
        } else {
          final double existingWidth = splitChildBounds.getWidth();
          final Rectangle newSplitChildBounds = boundsWithXandWidth(bounds, x, existingWidth);
          layout2(splitChild, newSplitChildBounds);
        }
        x = splitChild.getBounds().getMaxX();
      }
    }

    /* Layout the Split's child Nodes' along the Y axis.  The bounds
       * of each child will have the same x coordinate and width as the
       * layoutGrow() bounds argument.  Extra height is allocated to the
       * to each child with a non-zero weight:
       *     newHeight = currentHeight + (extraHeight * splitChild.getWeight())
       * Any extraHeight "left over" (that's availableHeight in the loop
       * below) is given to the last child.  Note that Dividers always
       * have a weight of zero, and they're never the last child.
       */
    else {
      double y = bounds.getY();
      final double extraHeight = bounds.getMaxY() - splitBounds.getHeight();
      double availableHeight = extraHeight;

      while (splitChildren.hasNext()) {
        final Node splitChild = splitChildren.next();
        final Rectangle splitChildBounds = splitChild.getBounds();
        final double splitChildWeight = splitChild.getWeight();

        if (!splitChildren.hasNext()) {
          final double newHeight = bounds.getMaxY() - y;
          final Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
          layout2(splitChild, newSplitChildBounds);
        } else if (availableHeight > 0.0 && splitChildWeight > 0.0) {
          final double allocatedHeight = splitChild.equals(lastWeightedChild) ? availableHeight : Math.rint(splitChildWeight * extraHeight);
          final double newHeight = splitChildBounds.getHeight() + allocatedHeight;
          final Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, newHeight);
          layout2(splitChild, newSplitChildBounds);
          availableHeight -= allocatedHeight;
        } else {
          final double existingHeight = splitChildBounds.getHeight();
          final Rectangle newSplitChildBounds = boundsWithYandHeight(bounds, y, existingHeight);
          layout2(splitChild, newSplitChildBounds);
        }
        y = splitChild.getBounds().getMaxY();
      }
    }
  }


  /* Second pass of the layout algorithm: branch to layoutGrow/Shrink
     * as needed.
     */
  private void layout2(final Node root, final Rectangle bounds) {
    if (root instanceof Leaf) {
      final Component child = childForNode(root);
      if (child != null) {
        child.setBounds(bounds);
      }
      root.setBounds(bounds);
    } else if (root instanceof Divider) {
      root.setBounds(bounds);
    } else if (root instanceof Split) {
      final Split split = (Split) root;
      final boolean grow = split.isRowLayout() ? split.getBounds().width <= bounds.width : split.getBounds().height <= bounds.height;
      if (grow) {
        layoutGrow(split, bounds);
        root.setBounds(bounds);
      } else {
        layoutShrink(split, bounds);
        // split.setBounds() called in layoutShrink()
      }
    }
  }


  /* First pass of the layout algorithm.
     *
     * If the Dividers are "floating" then set the bounds of each
     * node to accomodate the preferred size of all of the
     * Leaf's java.awt.Components.  Otherwise, just set the bounds
     * of each Leaf/Split node so that it's to the left of (for
     * Split.isRowLayout() Split children) or directly above
     * the Divider that follows.
     *
     * This pass sets the bounds of each Node in the layout model.  It
     * does not resize any of the parent Container's
     * (java.awt.Component) children.  That's done in the second pass,
     * see layoutGrow() and layoutShrink().
     */
  private void layout1(final Node root, final Rectangle bounds) {
    if (root instanceof Leaf) {
      root.setBounds(bounds);
    } else if (root instanceof Split) {
      final Split split = (Split) root;
      final Iterator<Node> splitChildren = split.getChildren().iterator();
      Rectangle childBounds;
      final int dividerSize = getDividerSize();

      /* Layout the Split's child Nodes' along the X axis.  The bounds
           * of each child will have the same y coordinate and height as the
           * layout1() bounds argument.
           *
           * Note: the column layout code - that's the "else" clause below
           * this if, is identical to the X axis (rowLayout) code below.
           */
      if (split.isRowLayout()) {
        double x = bounds.getX();
        while (splitChildren.hasNext()) {
          final Node splitChild = splitChildren.next();
          final Divider dividerChild = splitChildren.hasNext() ? (Divider) splitChildren.next() : null;

          final double childWidth;
          if (getFloatingDividers()) {
            childWidth = preferredNodeSize(splitChild).getWidth();
          } else {
            if (dividerChild != null) {
              childWidth = dividerChild.getBounds().getX() - x;
            } else {
              childWidth = split.getBounds().getMaxX() - x;
            }
          }
          childBounds = boundsWithXandWidth(bounds, x, childWidth);
          layout1(splitChild, childBounds);

          if (getFloatingDividers() && dividerChild != null) {
            final double dividerX = childBounds.getMaxX();
            final Rectangle dividerBounds = boundsWithXandWidth(bounds, dividerX, dividerSize);
            dividerChild.setBounds(dividerBounds);
          }
          if (dividerChild != null) {
            x = dividerChild.getBounds().getMaxX();
          }
        }
      }

      /* Layout the Split's child Nodes' along the Y axis.  The bounds
           * of each child will have the same x coordinate and width as the
           * layout1() bounds argument.  The algorithm is identical to what's
           * explained above, for the X axis case.
           */
      else {
        double y = bounds.getY();
        while (splitChildren.hasNext()) {
          final Node splitChild = splitChildren.next();
          final Divider dividerChild = splitChildren.hasNext() ? (Divider) splitChildren.next() : null;

          final double childHeight;
          if (getFloatingDividers()) {
            childHeight = preferredNodeSize(splitChild).getHeight();
          } else {
            if (dividerChild != null) {
              childHeight = dividerChild.getBounds().getY() - y;
            } else {
              childHeight = split.getBounds().getMaxY() - y;
            }
          }
          childBounds = boundsWithYandHeight(bounds, y, childHeight);
          layout1(splitChild, childBounds);

          if (getFloatingDividers() && dividerChild != null) {
            final double dividerY = childBounds.getMaxY();
            final Rectangle dividerBounds = boundsWithYandHeight(bounds, dividerY, dividerSize);
            dividerChild.setBounds(dividerBounds);
          }
          if (dividerChild != null) {
            y = dividerChild.getBounds().getMaxY();
          }
        }
      }
      /* The bounds of the Split node root are set to be just
           * big enough to contain all of its children, but only
           * along the axis it's allocating space on.  That's
           * X for rows, Y for columns.  The second pass of the
           * layout algorithm - see layoutShrink()/layoutGrow()
           * allocates extra space.
           */
      minimizeSplitBounds(split, bounds);
    }
  }


  /**
   * The specified Node is either the wrong type or was configured
   * incorrectly.
   */
  public static class InvalidLayoutException extends RuntimeException {

    private static final long serialVersionUID = 0L;

    private final transient Node node;


    public InvalidLayoutException(final String msg, final Node node) {
      super(msg);
      this.node = node;
    }


    /** @return the invalid Node. */
    public Node getNode() {
      return node;
    }
  }


  private static void throwInvalidLayout(final String msg, final Node node) {
    throw new InvalidLayoutException(msg, node);
  }


  private static void checkLayout(final Node root) {
    if (root instanceof Split) {
      final Split split = (Split) root;
      //if (split.getChildren().size() <= 2) {
        //throwInvalidLayout("Split must have > 2 children", root);
      //}
      final Iterator<Node> splitChildren = split.getChildren().iterator();
      double weight = 0.0;
      while (splitChildren.hasNext()) {
        final Node splitChild = splitChildren.next();
        if (splitChild instanceof Divider) {
          throwInvalidLayout("expected a Split or Leaf Node", splitChild);
        }
        if (splitChildren.hasNext()) {
          final Node dividerChild = splitChildren.next();
          if (!(dividerChild instanceof Divider)) {
            throwInvalidLayout("expected a Divider Node", dividerChild);
          }
        }
        weight += splitChild.getWeight();
        checkLayout(splitChild);
      }
      if (weight > 1.0) {
        throwInvalidLayout("Split children's total weight > 1.0", root);
      }
    }
  }


  /**
   * Compute the bounds of all of the Split/Divider/Leaf Nodes in
   * the layout model, and then set the bounds of each child component
   * with a matching Leaf Node.
   */
  public void layoutContainer(final Container parent) {
    checkLayout(getModel());
    final Insets insets = parent.getInsets();
    final Dimension size = parent.getSize();
    final int width = size.width - (insets.left + insets.right);
    final int height = size.height - (insets.top + insets.bottom);
    final Rectangle bounds = new Rectangle(insets.left, insets.top, width, height);
    layout1(getModel(), bounds);
    layout2(getModel(), bounds);
  }


  private static Divider dividerAt(final Node root, final int x, final int y) {
    if (root instanceof Divider) {
      final Divider divider = (Divider) root;
      return divider.getBounds().contains(x, y) ? divider : null;
    } else if (root instanceof Split) {
      final Split split = (Split) root;
      for (final Node child : split.getChildren()) {
        if (child.getBounds().contains(x, y)) {
          return dividerAt(child, x, y);
        }
      }
    }
    return null;
  }


  /**
   * Return the Divider whose bounds contain the specified
   * point, or null if there isn't one.
   *
   * @param x x coordinate
   * @param y y coordinate
   * @return the Divider at x,y
   */
  public Divider dividerAt(final int x, final int y) {
    return dividerAt(getModel(), x, y);
  }


  private static boolean nodeOverlapsRectangle(final Node node, final Rectangle r2) {
    final Rectangle r1 = node.getBounds();
    return r1.x <= r2.x + r2.width && r1.x + r1.width >= r2.x && r1.y <= r2.y + r2.height && r1.y + r1.height >= r2.y;
  }


  private static List<Divider> dividersThatOverlap(final Node root, final Rectangle r) {
    if (nodeOverlapsRectangle(root, r) && root instanceof Split) {
      final List<Divider> dividers = new ArrayList<Divider>();
      for (final Node child : ((Split) root).getChildren()) {
        if (child instanceof Divider) {
          if (nodeOverlapsRectangle(child, r)) {
            dividers.add((Divider) child);
          }
        } else if (child instanceof Split) {
          dividers.addAll(dividersThatOverlap(child, r));
        }
      }
      return dividers;
    } else {
      return Collections.emptyList();
    }
  }


  /**
   * Return the Dividers whose bounds overlap the specified
   * Rectangle.
   *
   * @param r target Rectangle
   * @return the Dividers that overlap r
   * @throws IllegalArgumentException if the Rectangle is null
   */
  public Iterable<Divider> dividersThatOverlap(final Rectangle r) {
    if (r == null) {
      throw new IllegalArgumentException("null Rectangle");
    }
    return dividersThatOverlap(getModel(), r);
  }


  /** Base class for the nodes that model a MultiSplitLayout. */
  public abstract static class Node {

    private Split parent;
    private Rectangle bounds = new Rectangle();
    private double weight;


    /**
     * Returns the Split parent of this Node, or null.
     *
     * @return the value of the parent property.
     * @see #setParent
     */
    public Split getParent() {
      return parent;
    }


    /**
     * Set the value of this Node's parent property.  The default
     * value of this property is null.
     *
     * @param parent a Split or null
     * @see #getParent
     */
    public void setParent(final Split parent) {
      this.parent = parent;
    }


    /**
     * Returns the bounding Rectangle for this Node.
     *
     * @return the value of the bounds property.
     * @see #setBounds
     */
    public Rectangle getBounds() {
      return new Rectangle(bounds);
    }


    /**
     * Set the bounding Rectangle for this node.  The value of
     * bounds may not be null.  The default value of bounds
     * is equal to <code>new Rectangle(0,0,0,0)</code>.
     *
     * @param bounds the new value of the bounds property
     * @throws IllegalArgumentException if bounds is null
     * @see #getBounds
     */
    public void setBounds(final Rectangle bounds) {
      if (bounds == null) {
        throw new IllegalArgumentException("null bounds");
      }
      this.bounds = new Rectangle(bounds);
    }


    /**
     * Value between 0.0 and 1.0 used to compute how much space
     * to add to this sibling when the layout grows or how
     * much to reduce when the layout shrinks.
     *
     * @return the value of the weight property
     * @see #setWeight
     */
    public double getWeight() {
      return weight;
    }


    /**
     * The weight property is a between 0.0 and 1.0 used to
     * compute how much space to add to this sibling when the
     * layout grows or how much to reduce when the layout shrinks.
     * If rowLayout is true then this node's width grows
     * or shrinks by (extraSpace * weight).  If rowLayout is false,
     * then the node's height is changed.  The default value
     * of weight is 0.0.
     *
     * @param weight a double between 0.0 and 1.0
     * @throws IllegalArgumentException if weight is not between 0.0 and 1.0
     * @see #getWeight
     * @see MultiSplitLayout#layoutContainer
     */
    public void setWeight(final double weight) {
      if (weight < 0.0 || weight > 1.0) {
        throw new IllegalArgumentException("invalid weight");
      }
      this.weight = weight;
    }


    private Node siblingAtOffset(final int offset) {
      final Split parent = getParent();
      if (parent == null) {
        return null;
      }
      final List<Node> siblings = parent.getChildren();
      int index = siblings.indexOf(this);
      if (index == -1) {
        return null;
      }
      index += offset;
      return index > -1 && index < siblings.size() ? siblings.get(index) : null;
    }


    /**
     * Return the Node that comes after this one in the parent's
     * list of children, or null.  If this node's parent is null,
     * or if it's the last child, then return null.
     *
     * @return the Node that comes after this one in the parent's list of children.
     * @see #previousSibling
     * @see #getParent
     */
    public Node nextSibling() {
      return siblingAtOffset(+1);
    }


    /**
     * Return the Node that comes before this one in the parent's
     * list of children, or null.  If this node's parent is null,
     * or if it's the last child, then return null.
     *
     * @return the Node that comes before this one in the parent's list of children.
     * @see #nextSibling
     * @see #getParent
     */
    public Node previousSibling() {
      return siblingAtOffset(-1);
    }
  }

  /**
   * Defines a vertical or horizontal subdivision into two or more
   * tiles.
   */
  public static class Split extends Node {

    private List<Node> children = Collections.emptyList();
    private boolean rowLayout = true;


    /**
     * Returns true if the this Split's children are to be
     * laid out in a row: all the same height, left edge
     * equal to the previous Node's right edge.  If false,
     * children are laid on in a column.
     *
     * @return the value of the rowLayout property.
     * @see #setRowLayout
     */
    public boolean isRowLayout() {
      return rowLayout;
    }


    /**
     * Set the rowLayout property.  If true, all of this Split's
     * children are to be laid out in a row: all the same height,
     * each node's left edge equal to the previous Node's right
     * edge.  If false, children are laid on in a column.  Default
     * value is true.
     *
     * @param rowLayout true for horizontal row layout, false for column
     * @see #isRowLayout
     */
    public void setRowLayout(final boolean rowLayout) {
      this.rowLayout = rowLayout;
    }


    /**
     * Returns this Split node's children.  The returned value
     * is not a reference to the Split's internal list of children
     *
     * @return the value of the children property.
     * @see #setChildren
     */
    public List<Node> getChildren() {
      return new ArrayList<Node>(children);
    }


    /**
     * Set's the children property of this Split node.  The parent
     * of each new child is set to this Split node, and the parent
     * of each old child (if any) is set to null.  This method
     * defensively copies the incoming List.  Default value is
     * an empty List.
     *
     * @param children List of children
     * @throws IllegalArgumentException if children is null
     * @see #getChildren
     */
    public void setChildren(final List<Node> children) {
      if (children == null) {
        throw new IllegalArgumentException("children must be a non-null List");
      }
      for (final Node child : this.children) {
        child.setParent(null);
      }
      this.children = new ArrayList<Node>(children);
      for (final Node child : this.children) {
        child.setParent(this);
      }
    }


    /**
     * Convenience method that returns the last child whose weight
     * is > 0.0.
     *
     * @return the last child whose weight is > 0.0.
     * @see #getChildren
     * @see Node#getWeight
     */
    public final Node lastWeightedChild() {
      final List<Node> children = getChildren();
      Node weightedChild = null;
      for (final Node child : children) {
        if (child.getWeight() > 0.0) {
          weightedChild = child;
        }
      }
      return weightedChild;
    }


    @Override
    public String toString() {
      final StringBuilder sb = new StringBuilder();
      sb.append("Split");
      sb.append("{children=").append(children);
      sb.append(", rowLayout=").append(rowLayout);
      sb.append('}');
      return sb.toString();
    }
  }


  /** Models a java.awt Component child. */
  public static class Leaf extends Node {

    private String name = "";


    /** Create a Leaf node.  The default value of name is "". */
    public Leaf() {
    }


    /**
     * Create a Leaf node with the specified name.  Name can not
     * be null.
     *
     * @param name value of the Leaf's name property
     * @throws IllegalArgumentException if name is null
     */
    public Leaf(final String name) {
      if (name == null) {
        throw new IllegalArgumentException("name is null");
      }
      this.name = name;
    }


    /**
     * Return the Leaf's name.
     *
     * @return the value of the name property.
     * @see #setName
     */
    public String getName() {
      return name;
    }


    /**
     * Set the value of the name property.  Name may not be null.
     *
     * @param name value of the name property
     * @throws IllegalArgumentException if name is null
     */
    public void setName(final String name) {
      if (name == null) {
        throw new IllegalArgumentException("name is null");
      }
      this.name = name;
    }


    @Override
    public String toString() {
      final StringBuilder sb = new StringBuilder();
      sb.append("Leaf");
      sb.append("{name='").append(name).append('\'');
      sb.append('}');
      return sb.toString();
    }
  }


  /** Models a single vertical/horiztonal divider. */
  public static class Divider extends Node {

    /**
     * Convenience method, returns true if the Divider's parent
     * is a Split row (a Split with isRowLayout() true), false
     * otherwise. In other words if this Divider's major axis
     * is vertical, return true.
     *
     * @return true if this Divider is part of a Split row.
     */
    public final boolean isVertical() {
      final Split parent = getParent();
      return parent != null && parent.isRowLayout();
    }


    /**
     * Dividers can't have a weight, they don't grow or shrink.
     *
     * @throws UnsupportedOperationException
     */
    @Override
    public void setWeight(final double weight) {
      throw new UnsupportedOperationException();
    }


    @Override
    public String toString() {
      return "MultiSplitLayout.Divider " + getBounds().toString();
    }
  }


  private static void throwParseException(final StreamTokenizer st, final String msg) throws Exception {
    throw new Exception("MultiSplitLayout.parseModel Error: " + msg);
  }


  private static void parseAttribute(final String name, final StreamTokenizer st, final Node node) throws Exception {
    if (st.nextToken() != '=') {
      throwParseException(st, "expected '=' after " + name);
    }
    if ("WEIGHT".equalsIgnoreCase(name)) {
      if (st.nextToken() == StreamTokenizer.TT_NUMBER) {
        node.setWeight(st.nval);
      } else {
        throwParseException(st, "invalid weight");
      }
    } else if ("NAME".equalsIgnoreCase(name)) {
      if (st.nextToken() == StreamTokenizer.TT_WORD) {
        if (node instanceof Leaf) {
          ((Leaf) node).setName(st.sval);
        } else {
          throwParseException(st, "can't specify name for " + node);
        }
      } else {
        throwParseException(st, "invalid name");
      }
    } else {
      throwParseException(st, "unrecognized attribute \"" + name + '"');
    }
  }


  private static void addSplitChild(final Split parent, final Node child) {
    final List<Node> children = new ArrayList<Node>(parent.getChildren());
    if (children.isEmpty()) {
      children.add(child);
    } else {
      children.add(new Divider());
      children.add(child);
    }
    parent.setChildren(children);
  }


  private static void parseLeaf(final StreamTokenizer st, final Split parent) throws Exception {
    final Leaf leaf = new Leaf();
    int token;
    while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) {
      if (token == ')') {
        break;
      }
      if (token == StreamTokenizer.TT_WORD) {
        parseAttribute(st.sval, st, leaf);
      } else {
        throwParseException(st, "Bad Leaf: " + leaf);
      }
    }
    addSplitChild(parent, leaf);
  }


  private static void parseSplit(final StreamTokenizer st, final Split parent) throws Exception {
    int token;
    while ((token = st.nextToken()) != StreamTokenizer.TT_EOF) {
      if (token == ')') {
        break;
      } else if (token == StreamTokenizer.TT_WORD) {
        if ("WEIGHT".equalsIgnoreCase(st.sval)) {
          parseAttribute(st.sval, st, parent);
        } else {
          addSplitChild(parent, new Leaf(st.sval));
        }
      } else if (token == '(') {
        if (st.nextToken() != StreamTokenizer.TT_WORD) {
          throwParseException(st, "invalid node type");
        }
        final String nodeType = st.sval.toUpperCase(Locale.ENGLISH);
        if ("LEAF".equals(nodeType)) {
          parseLeaf(st, parent);
        } else if ("ROW".equals(nodeType) || "COLUMN".equals(nodeType)) {
          final Split split = new Split();
          split.setRowLayout("ROW".equals(nodeType));
          addSplitChild(parent, split);
          parseSplit(st, split);
        } else {
          throwParseException(st, "unrecognized node type '" + nodeType + '\'');
        }
      }
    }
  }


  private static Node parseModel(final Reader r) {
    final StreamTokenizer st = new StreamTokenizer(r);
    try {
      final Split root = new Split();
      parseSplit(st, root);
      return root.getChildren().get(0);
    } catch (final Exception e) {
      //noinspection UseOfSystemOutOrSystemErr
      System.err.println(e);
    } finally {
      try {
        r.close();
      } catch (final IOException ignore) {
      }
    }
    return null;
  }


  /**
   * A convenience method that converts a string to a
   * MultiSplitLayout model (a tree of Nodes) using a
   * a simple syntax.  Nodes are represented by
   * parenthetical expressions whose first token
   * is one of ROW/COLUMN/LEAF.  ROW and COLUMN specify
   * horizontal and vertical Split nodes respectively,
   * LEAF specifies a Leaf node.  A Leaf's name and
   * weight can be specified with attributes,
   * name=<i>myLeafName</i> weight=<i>myLeafWeight</i>.
   * Similarly, a Split's weight can be specified with
   * weight=<i>mySplitWeight</i>.
   * <p/>
   * <p> For example, the following expression generates
   * a horizontal Split node with three children:
   * the Leafs named left and right, and a Divider in
   * between:
   * <pre>
   * (ROW (LEAF name=left) (LEAF name=right weight=1.0))
   * </pre>
   * <p/>
   * <p> Dividers should not be included in the string,
   * they're added automatcially as needed.  Because
   * Leaf nodes often only need to specify a name, one
   * can specify a Leaf by just providing the name.
   * The previous example can be written like this:
   * <pre>
   * (ROW left (LEAF name=right weight=1.0))
   * </pre>
   * <p/>
   * <p>Here's a more complex example.  One row with
   * three elements, the first and last of which are columns
   * with two leaves each:
   * <pre>
   * (ROW (COLUMN weight=0.5 left.top left.bottom)
   *      (LEAF name=middle)
   *      (COLUMN weight=0.5 right.top right.bottom))
   * </pre>
   * <p/>
   * <p/>
   * <p> This syntax is not intended for archiving or
   * configuration files .  It's just a convenience for
   * examples and tests.
   *
   * @param s
   * @return the Node root of a tree based on s.
   */
  public static Node parseModel(final String s) {
    return parseModel(new StringReader(s));
  }


  private static void printModel(final String indent, final Node root) {
    if (root instanceof Split) {
      final Split split = (Split) root;
      //noinspection UseOfSystemOutOrSystemErr
      System.out.println(indent + split);
      for (final Node child : split.getChildren()) {
        printModel(indent + "  ", child);
      }
    } else {
      //noinspection UseOfSystemOutOrSystemErr
      System.out.println(indent + root);
    }
  }


  /** Print the tree with enough detail for simple debugging.
   * @param root*/
  public static void printModel(final Node root) {
    printModel("", root);
  }
}
TOP

Related Classes of org.twodividedbyzero.idea.findbugs.gui.common.MultiSplitLayout$Split

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.