All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cobraparser.html.renderer.TableMatrix Maven / Gradle / Ivy

There is a newer version: 1.0.2
Show newest version
/*
GNU LESSER GENERAL PUBLIC LICENSE
    Copyright (C) 2006 The Lobo Project

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    Contact info: [email protected]
 */
package org.cobraparser.html.renderer;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.cobraparser.html.HtmlRendererContext;
import org.cobraparser.html.domimpl.AnonymousNodeImpl;
import org.cobraparser.html.domimpl.HTMLElementImpl;
import org.cobraparser.html.domimpl.ModelNode;
import org.cobraparser.html.domimpl.NodeImpl;
import org.cobraparser.html.domimpl.TextImpl;
import org.cobraparser.html.style.BorderInfo;
import org.cobraparser.html.style.HtmlInsets;
import org.cobraparser.html.style.HtmlLength;
import org.cobraparser.html.style.HtmlValues;
import org.cobraparser.html.style.JStyleProperties;
import org.cobraparser.html.style.RenderState;
import org.cobraparser.html.style.RenderThreadState;
import org.cobraparser.ua.UserAgentContext;

final class TableMatrix {
  private final ArrayList ROWS = new ArrayList<>();
  private final ArrayList ROW_GROUPS = new ArrayList<>();
  private final ArrayList<@NonNull RAbstractCell> ALL_CELLS = new ArrayList<>();
  private final HTMLElementImpl tableElement;
  private final UserAgentContext uaContext;
  private final HtmlRendererContext rendererContext;
  private final FrameContext frameContext;
  private final RElement relement;
  private final RenderableContainer container;

  private ColSizeInfo[] columnSizes;
  private RowSizeInfo[] rowSizes;
  private int tableWidth;
  private int tableHeight;

  /*
   * This is so that we can draw the lines inside the table that appear when a
   * border attribute is used.
   */
  private int hasOldStyleBorder;

  /**
   * @param element
   */
  public TableMatrix(final HTMLElementImpl element, final UserAgentContext uaContext, final HtmlRendererContext rcontext,
      final FrameContext frameContext,
      final RenderableContainer tableAsContainer, final RElement relement) {
    this.tableElement = element;
    this.uaContext = uaContext;
    this.rendererContext = rcontext;
    this.frameContext = frameContext;
    this.relement = relement;
    this.container = tableAsContainer;
  }

  @Override
  public void finalize() throws Throwable {
    super.finalize();
  }

  public int getNumRows() {
    return this.ROWS.size();
  }

  public int getNumColumns() {
    return this.columnSizes.length;
  }

  /**
   * @return Returns the tableHeight.
   */
  public int getTableHeight() {
    return this.tableHeight;
  }

  /**
   * @return Returns the tableWidth.
   */
  public int getTableWidth() {
    return this.tableWidth;
  }

  // private int border;
  private int cellSpacingY;
  private int cellSpacingX;
  private int widthsOfExtras;
  private int heightsOfExtras;
  private HtmlLength tableWidthLength;
  private ArrayList rowGroupSizes;

  /**
   * Called on every relayout. Element children might have changed.
   */
  public void reset(final Insets insets, final int availWidth, final int availHeight) {
    // TODO: Incorporate into build() and calculate
    // sizes properly based on parameters.
    ROW_GROUPS.clear();
    ROWS.clear();
    ALL_CELLS.clear();
    rowGroupSizes = null;
    // TODO: Does it need this old-style border?
    final int border = getBorderAttribute();
    final int cellSpacing = getCellSpacingAttribute();

    this.cellSpacingX = cellSpacing;
    this.cellSpacingY = cellSpacing;

    this.tableWidthLength = TableMatrix.getWidthLength(this.tableElement, availWidth);

    this.populateRows();
    this.adjustForCellSpans();
    this.createSizeArrays();

    // Calculate widths of extras
    final ColSizeInfo[] columnSizes = this.columnSizes;
    final int numCols = columnSizes.length;
    int widthsOfExtras = insets.left + insets.right + ((numCols + 1) * cellSpacing);
    if (border > 0) {
      widthsOfExtras += (numCols * 2);
    }
    this.widthsOfExtras = widthsOfExtras;

    // Calculate heights of extras
    final RowSizeInfo[] rowSizes = this.rowSizes;
    final int numRows = rowSizes.length;
    int heightsOfExtras = insets.top + insets.bottom + ((numRows + 1) * cellSpacing);
    if (border > 0) {
      heightsOfExtras += (numRows * 2);
    }
    this.heightsOfExtras = heightsOfExtras;
    this.hasOldStyleBorder = border > 0 ? 1 : 0;
  }

  private int getCellSpacingAttribute() {
    int cellSpacing = 0;
    final String cellSpacingText = this.tableElement.getAttribute("cellspacing");
    if (cellSpacingText != null) {
      try {
        // TODO: cellSpacing can be a percentage as well
        cellSpacing = Integer.parseInt(cellSpacingText);
        if (cellSpacing < 0) {
          cellSpacing = 0;
        }
      } catch (final NumberFormatException nfe) {
        System.out.println("Exception while parsing cellSpacing: " + nfe);
        // ignore
      }
    }
    return cellSpacing;
  }

  private int getBorderAttribute() {
    int border = 0;
    final String borderText = this.tableElement.getAttribute("border");
    if (borderText != null) {
      if (borderText.length() == 0) {
        border = 1;
      } else {
        try {
          border = Integer.parseInt(borderText);
          if (border < 0) {
            border = 0;
          }
        } catch (final NumberFormatException nfe) {
          System.out.println("Exception while parsing border: " + nfe);
          // ignore
        }
      }
    }
    return border;
  }

  public void build(final int availWidth, final int availHeight, final boolean sizeOnly) {
    final int hasBorder = this.hasOldStyleBorder;
    this.determineColumnSizes(hasBorder, this.cellSpacingX, this.cellSpacingY, availWidth);
    this.determineRowSizes(hasBorder, this.cellSpacingY, availHeight, sizeOnly);
  }

  private static HtmlLength getWidthLength(final HTMLElementImpl element, final int availWidth) {
    try {
      final JStyleProperties props = element.getCurrentStyle();
      final String widthText = props.getWidth();
      if (widthText == null) {
        // TODO: convert attributes to CSS properties
        final String widthAttr = element.getAttribute("width");
        if (widthAttr == null) {
          return null;
        }
        return new HtmlLength(HtmlValues.getPixelSize(widthAttr, element.getRenderState(), 0, availWidth));
      } else {
        return new HtmlLength(HtmlValues.getPixelSize(widthText, element.getRenderState(), 0, availWidth));
      }
    } catch (final NumberFormatException err) {
      System.out.println("Exception while parsing width: " + err);
      return null;
    }
  }

  private static HtmlLength getHeightLength(final HTMLElementImpl element, final int availHeight) {
    try {
      final JStyleProperties props = element.getCurrentStyle();
      final String heightText = props.getHeight();
      if (heightText == null) {
        final String ha = element.getAttribute("height");
        if (ha == null) {
          return null;
        } else {
          return new HtmlLength(HtmlValues.getPixelSize(ha, element.getRenderState(), 0, availHeight));
        }
      } else {
        return new HtmlLength(HtmlValues.getPixelSize(heightText, element.getRenderState(), 0, availHeight));
      }
    } catch (final NumberFormatException err) {
      System.out.println("Exception while parsing height: " + err);
      return null;
    }
  }

  static Insets getCSSInsets(final RenderState rs) {
    final BorderInfo borderInfo = rs.getBorderInfo();
    final HtmlInsets elemBorderHtmlInsets = borderInfo == null ? null : borderInfo.insets;
    return elemBorderHtmlInsets == null ? RBlockViewport.ZERO_INSETS : elemBorderHtmlInsets.getAWTInsets(0, 0, 0, 0, 0, 0, 0, 0);
  }

  private static final class RowGroup {
    final ArrayList rows = new ArrayList<>();
    private final HTMLElementImpl rowGroupElem;
    final BorderOverrider borderOverrider = new BorderOverrider();

    public RowGroup(final HTMLElementImpl rowGroupElem) {
      this.rowGroupElem = rowGroupElem;
    }

    void add(final Row row) {
      rows.add(row);
      row.rowGroup = this;
    }

    public void finish() {
      final int numRows = rows.size();
      int minCellBorderLeft = -1;
      int minCellBorderRight = -1;
      for (int i = 0; i < numRows; i++) {
        final Row r = rows.get(i);
        final int cellBorderLeftMost = r.getCellBorderLeftMost();
        if ((minCellBorderLeft == -1) || (cellBorderLeftMost < minCellBorderLeft)) {
          minCellBorderLeft = cellBorderLeftMost;
        }
        final int cellBorderRightMost = r.getCellBorderRightMost();
        if ((minCellBorderRight == -1) || (cellBorderRightMost < minCellBorderRight)) {
          minCellBorderRight = cellBorderRightMost;
        }
      }
      final int minCellBorderTop = rows.get(0).minCellBorderTop;
      final int minCellBorderBottom = rows.get(0).minCellBorderBottom;

      final Insets groupBorderInsets = rowGroupElem == null ? null : getCSSInsets(rowGroupElem.getRenderState());

      if (groupBorderInsets != null) {
        if (groupBorderInsets.top <= minCellBorderTop) {
          borderOverrider.topOverridden = true;
        } else {
          final Row firstRow = rows.get(0);
          for (final VirtualCell cell : firstRow.cells) {
            // TODO: Only override if cells border is less than minCellBorderTop (?)
            cell.getActualCell().borderOverrider.topOverridden = true;
          }
        }

        if (groupBorderInsets.bottom <= minCellBorderBottom) {
          borderOverrider.bottomOverridden = true;
        } else {
          final Row lastRow = rows.get(rows.size() - 1);
          for (final VirtualCell cell : lastRow.cells) {
            // TODO: Only override if cells border is less than minCellBorderBottom (?)
            cell.getActualCell().borderOverrider.bottomOverridden = true;
          }
        }

        if (groupBorderInsets.left <= minCellBorderLeft) {
          borderOverrider.leftOverridden = true;
        } else {
          for (final Row row : rows) {
            row.getLeftMostCell().getActualCell().borderOverrider.leftOverridden = true;
          }
        }
        if (groupBorderInsets.right <= minCellBorderRight) {
          borderOverrider.rightOverridden = true;
        } else {
          for (final Row row : rows) {
            row.getRightMostCell().getActualCell().borderOverrider.rightOverridden = true;
          }
        }
      }
    }

    @Nullable HtmlInsets getGroupBorderInsets() {
      final BorderInfo borderInfo = rowGroupElem == null ? null : rowGroupElem.getRenderState().getBorderInfo();
      return borderInfo == null ? null : borderOverrider.get(borderInfo.insets);
    }

  }

  private static final class Row {
    final ArrayList cells = new ArrayList<>();
    final HTMLElementImpl rowGroupElem;
    RowGroup rowGroup;

    // TODO: Add getters and make private for the following four
    public boolean firstInGroup;
    public boolean lastInGroup;
    public int maxCellBorderTop = 0;
    public int maxCellBorderBottom = 0;
    int minCellBorderBottom = -1;
    int minCellBorderTop = -1;
    int rowIndex;

    Row(final HTMLElementImpl rowGroup) {
      this.rowGroupElem = rowGroup;
    }

    VirtualCell getLeftMostCell() {
      return cells.get(0);
    }

    VirtualCell getRightMostCell() {
      return cells.get(cells.size() - 1);
    }

    int getCellBorderRightMost() {
      return getCSSInsets(getLeftMostCell().getActualCell().getRenderState()).right;
    }

    int getCellBorderLeftMost() {
      return getCSSInsets(getLeftMostCell().getActualCell().getRenderState()).left;
    }

    void add(final @Nullable VirtualCell cell) {
      if (cell != null) {
        final RAbstractCell ac = cell.getActualCell();
        final @NonNull RenderState rs = ac.getRenderState();
        BorderInfo binfo = rs.getBorderInfo();
        if (binfo != null) {
          final HtmlInsets bi = binfo.insets;
          if (bi != null) {
            if (bi.top > maxCellBorderTop) {
              maxCellBorderTop = bi.top;
            }
            if ((bi.top < minCellBorderTop) || (minCellBorderTop == -1)) {
              minCellBorderTop = bi.top;
            }
            if (bi.bottom > maxCellBorderBottom) {
              maxCellBorderBottom = bi.bottom;
            }
            if ((bi.bottom < minCellBorderBottom) || (minCellBorderBottom == -1)) {
              minCellBorderBottom = bi.bottom;
            }
          }
        }
      }
      cells.add(cell);
    }

    public void add(int nc, VirtualCell virtualCell) {
      cells.add(nc, virtualCell);
    }

    public int size() {
      return cells.size();
    }

    public VirtualCell get(int c) {
      return cells.get(c);
    }
  }

  /** A class that helps map elements to children (or their delegates). It automatically takes care of
   *  non-existing parents by creating a place holder.
   *  For example, helps map table rows to virtual cells (which are delegates for table columns).
   */
  private static final class TableRelation {
    private final Map elementToRow = new HashMap<>(2);
    private Row currentFallbackRow = null;
    private final ArrayList listOfRows;
    private final ArrayList listOfRowGroups;

    public TableRelation(final ArrayList listOfRows, final ArrayList listOfRowGroups) {
      this.listOfRows = listOfRows;
      this.listOfRowGroups = listOfRowGroups;
    }

    void associate(final HTMLElementImpl rowGroupElem, final HTMLElementImpl rowElem, final VirtualCell cell) {
      Row row;
      if (rowElem != null) {
        currentFallbackRow = null;
        row = elementToRow.get(rowElem);
        if (row == null) {
          row = createRow(rowGroupElem);
          elementToRow.put(rowElem, row);
        }
      } else {
        // Doesn't have a parent. Let's add a list just for itself.
        if (currentFallbackRow != null) {
          row = currentFallbackRow;
        } else {
          row = createRow(rowGroupElem);
          currentFallbackRow = row;
        }
      }
      row.add(cell);
    }

    private Row createRow(final HTMLElementImpl rowGroupElem) {
      final Row row = new Row(rowGroupElem);
      row.rowIndex = this.listOfRows.size();
      this.listOfRows.add(row);
      return row;
    }

    void finish() {
      HTMLElementImpl prevRowGroupElem = null;
      RowGroup currentRowGroup = null;
      int numRows = listOfRows.size();
      for (int i = 0; i < numRows; i++)  {
        final Row row = listOfRows.get(i);
        row.firstInGroup = (i == 0) || (row.rowGroupElem != prevRowGroupElem);
        row.lastInGroup = (i == numRows - 1) || (listOfRows.get(i+1).rowGroupElem != row.rowGroupElem);
        if (row.firstInGroup) {
          currentRowGroup = new RowGroup(row.rowGroupElem);
          this.listOfRowGroups.add(currentRowGroup);
        }
        assert(currentRowGroup != null);
        currentRowGroup.add(row);
        if (row.lastInGroup) {
          currentRowGroup.finish();
        }
        prevRowGroupElem = row.rowGroupElem;
      }
    }
  }

  /**
   * Populates the ROWS and ALL_CELLS collections.
   */
  private ArrayList populateRows() {
    final HTMLElementImpl te = this.tableElement;
    final ArrayList rowElements = new ArrayList<>();
    final NodeImpl[] tChildren = te.getChildrenArray();
    final TableRelation rowRelation = new TableRelation(this.ROWS, this.ROW_GROUPS);

    if (tChildren != null) {
      for (final NodeImpl cn : tChildren) {
        if (cn instanceof HTMLElementImpl) {
          final HTMLElementImpl ce = (HTMLElementImpl) cn;
          final int display = ce.getRenderState().getDisplay();
          if (display == RenderState.DISPLAY_TABLE_ROW_GROUP || display == RenderState.DISPLAY_TABLE_HEADER_GROUP
              || display == RenderState.DISPLAY_TABLE_FOOTER_GROUP) {
            processRowGroup(ce, rowRelation);
          } else if (display == RenderState.DISPLAY_TABLE_ROW) {
            processRow(ce, null, rowRelation);
          } else if (display == RenderState.DISPLAY_TABLE_CELL) {
            processCell(ce, null, null, rowRelation);
          } else if (display != RenderState.DISPLAY_TABLE_COLUMN && display != RenderState.DISPLAY_TABLE_COLUMN_GROUP) {
            addAnonCell(rowRelation, null, null, cn);
          }
        } else if (cn instanceof TextImpl) {
          addAnonTextCell(rowRelation, null, null, (TextImpl) cn);
        }
      }
    }

    rowRelation.finish();

    {
      // Find the max insets among row group elements
      maxRowGroupLeft = 0;
      maxRowGroupRight = 0;
      for (final RowGroup rowGroup : this.ROW_GROUPS) {
        final HtmlInsets groupInsets = rowGroup.getGroupBorderInsets();
        if (groupInsets != null) {
          if (groupInsets.left > maxRowGroupLeft) {
            maxRowGroupLeft = groupInsets.left;
          }
          if (groupInsets.right > maxRowGroupRight) {
            maxRowGroupRight = groupInsets.right;
          }
        }
      }
    }

    return rowElements;
  }

  private void processCell(HTMLElementImpl ce, HTMLElementImpl rowGroupElem, HTMLElementImpl rowElem, TableRelation rowRelation) {
    RTableCell ac = new RTableCell(ce, this.uaContext, this.rendererContext, this.frameContext, this.container);
    ac.setParent(this.relement);
    ce.setUINode(ac);
    final VirtualCell vc = new VirtualCell(ac, true);
    ac.setTopLeftVirtualCell(vc);
    rowRelation.associate(rowGroupElem, rowElem, vc);
    this.ALL_CELLS.add(ac);
  }

  private void processRow(HTMLElementImpl rowE, HTMLElementImpl rowGroupElem, TableRelation rowRelation) {
    final NodeImpl[] rChildren = rowE.getChildrenArray();
    if (rChildren != null) {
      for (final NodeImpl cn : rChildren) {
        if (cn instanceof HTMLElementImpl) {
          final HTMLElementImpl ce = (HTMLElementImpl) cn;
          final int display = ce.getRenderState().getDisplay();
          if (display == RenderState.DISPLAY_TABLE_CELL) {
            processCell(ce, rowGroupElem, rowE, rowRelation);
          } else {
            addAnonCell(rowRelation, rowGroupElem, rowE, cn);
          }
        } else if (cn instanceof TextImpl) {
          addAnonTextCell(rowRelation, rowGroupElem, rowE, (TextImpl) cn);
        }
      }
    }
  }

  private void processRowGroup(HTMLElementImpl rowGroupElem, TableRelation rowRelation) {
    final NodeImpl[] rChildren = rowGroupElem.getChildrenArray();
    if (rChildren != null) {
      for (final NodeImpl cn : rChildren) {
        if (cn instanceof HTMLElementImpl) {
          final HTMLElementImpl ce = (HTMLElementImpl) cn;
          final int display = ce.getRenderState().getDisplay();
          if (display == RenderState.DISPLAY_TABLE_ROW) {
            processRow(ce, rowGroupElem, rowRelation);
          } else {
            addAnonCell(rowRelation, rowGroupElem, null, cn);
          }
        } else if (cn instanceof TextImpl) {
          addAnonTextCell(rowRelation, rowGroupElem, null, (TextImpl) cn);
        }
      }
    }
  }

  private void addAnonTextCell(final TableRelation rowRelation, HTMLElementImpl rowGroupElem, HTMLElementImpl rowElem, final TextImpl tn) {
    if (!tn.isElementContentWhitespace()) {
      addAnonCell(rowRelation, rowGroupElem, rowElem, tn);
    }
  }

  private void addAnonCell(final TableRelation rowRelation, HTMLElementImpl rowGroupElem, HTMLElementImpl rowElem, final NodeImpl node) {
    final AnonymousNodeImpl acn = new AnonymousNodeImpl(node.getParentNode());
    acn.appendChildSilently(node);
    final RAnonTableCell ac = new RAnonTableCell(acn, this.uaContext, this.rendererContext, this.frameContext, this.container);
    ac.setParent(this.relement);
    acn.setUINode(ac);
    final VirtualCell vc = new VirtualCell(ac, true);
    ac.setTopLeftVirtualCell(vc);
    rowRelation.associate(rowGroupElem, rowElem, vc);
    this.ALL_CELLS.add(ac);
  }

  /**
   * Based on colspans and rowspans, creates additional virtual cells from
   * actual table cells.
   */
  private void adjustForCellSpans() {
    final ArrayList rows = this.ROWS;
    int numRows = rows.size();
    for (int r = 0; r < numRows; r++) {
      final Row row = rows.get(r);
      int numCols = row.size();
      for (int c = 0; c < numCols; c++) {
        final VirtualCell vc = row.get(c);
        if ((vc != null) && vc.isTopLeft()) {
          final RAbstractCell ac = vc.getActualCell();
          int colspan = ac.getColSpan();
          if (colspan < 1) {
            colspan = 1;
          }
          int rowspan = ac.getRowSpan();
          if (rowspan < 1) {
            rowspan = 1;
          }

          // Can't go beyond last row (Fix bug #2022584)
          final int targetRows = r + rowspan;
          if (numRows < targetRows) {
            rowspan = numRows - r;
            ac.setRowSpan(rowspan);
          }

          numRows = rows.size();
          for (int y = 0; y < rowspan; y++) {
            if ((colspan > 1) || (y > 0)) {
              // Get row
              final int nr = r + y;
              final Row newRow = rows.get(nr);

              // Insert missing cells in row
              final int xstart = y == 0 ? 1 : 0;

              // Insert virtual cells, potentially
              // shifting others to the right.
              for (int cc = xstart; cc < colspan; cc++) {
                final int nc = c + cc;
                while (newRow.size() < nc) {
                  newRow.add(null);
                }
                newRow.add(nc, new VirtualCell(ac, false));
              }
              if (row == newRow) {
                numCols = row.size();
              }
            }
          }
        }
      }
    }

    // Adjust row and column of virtual cells
    for (int r = 0; r < numRows; r++) {
      final Row row = rows.get(r);
      final int numCols = row.size();
      for (int c = 0; c < numCols; c++) {
        final VirtualCell vc = row.get(c);
        if (vc != null) {
          vc.setColumn(c);
          vc.setRow(r);
        }
      }
    }
  }

  /**
   * Populates the columnSizes and rowSizes arrays, setting htmlLength in each
   * element.
   */
  private void createSizeArrays() {
    int numCols = 0;
    final ArrayList rows = this.ROWS;
    final int numRows = rows.size();

    {
      final RowSizeInfo[] rowSizes = new RowSizeInfo[numRows];
      this.rowSizes = rowSizes;
      for (int i = 0; i < numRows; i++) {
        final Row row = rows.get(i);
        final int numColsInThisRow = row.size();
        if (numColsInThisRow > numCols) {
          numCols = numColsInThisRow;
        }
        final RowSizeInfo rowSizeInfo = new RowSizeInfo();
        rowSizes[i] = rowSizeInfo;

        HtmlLength bestHeightLength = null;
        for (int x = 0; x < numColsInThisRow; x++) {
          final VirtualCell vc = row.get(x);
          if (vc != null) {
            final HtmlLength vcHeightLength = vc.getHeightLength();
            if ((vcHeightLength != null) && vcHeightLength.isPreferredOver(bestHeightLength)) {
              bestHeightLength = vcHeightLength;
            }
            rowSizeInfo.offsetX = maxRowGroupLeft;
          }
        }
        rowSizeInfo.htmlLength = bestHeightLength;

        @Nullable HtmlInsets rowGroupInsets = row.rowGroup.getGroupBorderInsets();
        if (row.firstInGroup && rowGroupInsets != null) {
          rowSizeInfo.marginTop = Math.max(0, rowGroupInsets.top);
        }
        if (row.lastInGroup && rowGroupInsets != null) {
          rowSizeInfo.marginBottom = Math.max(0, rowGroupInsets.bottom - row.maxCellBorderBottom);
        }
      }
    }

    final ColSizeInfo[] columnSizes = new ColSizeInfo[numCols];
    this.columnSizes = columnSizes;
    for (int i = 0; i < numCols; i++) {
      HtmlLength bestWidthLength = null;

      // Cells with colspan==1 first.
      for (int y = 0; y < numRows; y++) {
        final Row row = rows.get(y);
        VirtualCell vc;
        try {
          vc = row.get(i);
        } catch (final IndexOutOfBoundsException iob) {
          vc = null;
        }
        if (vc != null) {
          final RAbstractCell ac = vc.getActualCell();
          if (ac.getColSpan() == 1) {
            final HtmlLength vcWidthLength = vc.getWidthLength();
            if ((vcWidthLength != null) && vcWidthLength.isPreferredOver(bestWidthLength)) {
              bestWidthLength = vcWidthLength;
            }
          }
        }
      }
      // Now cells with colspan>1.
      if (bestWidthLength == null) {
        for (int y = 0; y < numRows; y++) {
          final Row row = rows.get(y);
          VirtualCell vc;
          try {
            vc = row.get(i);
          } catch (final IndexOutOfBoundsException iob) {
            vc = null;
          }
          if (vc != null) {
            final RAbstractCell ac = vc.getActualCell();
            if (ac.getColSpan() > 1) {
              final HtmlLength vcWidthLength = vc.getWidthLength();
              if ((vcWidthLength != null) && vcWidthLength.isPreferredOver(bestWidthLength)) {
                bestWidthLength = vcWidthLength;
              }
            }
          }
        }
      }
      final ColSizeInfo colSizeInfo = new ColSizeInfo();
      colSizeInfo.htmlLength = bestWidthLength;
      columnSizes[i] = colSizeInfo;
    }
  }

  /**
   * Determines the size of each column, and the table width. Does the
   * following:
   * 
    *
  1. Determine tentative widths. This is done by looking at declared column * widths, any table width, and filling in the blanks. No rendering is done. * The tentative width of columns with no declared width is zero. * *
  2. Render all cell blocks. It uses the tentative widths from the previous * step as a desired width. The resulting width is considered a sort of * minimum. If the column width is not defined, use a NOWRAP override flag to * render. * *
  3. Check if cell widths are too narrow for the rendered width. In the case * of columns without a declared width, check if they are too wide. * *
  4. Finally, adjust widths considering the expected max table size. Columns * are layed out again if necessary to determine if they can really be shrunk. *
* * @param renderState * @param border * @param cellSpacingX * @param cellSpacingY * @param availWidth */ private void determineColumnSizes(final int hasBorder, final int cellSpacingX, final int cellSpacingY, final int availWidth) { final HtmlLength tableWidthLength = this.tableWidthLength; int tableWidth; boolean widthKnown; if (tableWidthLength != null) { tableWidth = tableWidthLength.getLength(availWidth); widthKnown = true; } else { tableWidth = availWidth; widthKnown = false; } tableWidth -= (this.maxRowGroupLeft + this.maxRowGroupRight) / 2; final ColSizeInfo[] columnSizes = this.columnSizes; final int widthsOfExtras = this.widthsOfExtras; int cellAvailWidth = tableWidth - widthsOfExtras; if (cellAvailWidth < 0) { tableWidth += (-cellAvailWidth); cellAvailWidth = 0; } // Determine tentative column widths based on specified cell widths determineTentativeSizes(columnSizes, widthsOfExtras, cellAvailWidth, widthKnown); // Pre-layout cells. This will give the minimum width of each cell, // in addition to the minimum height. this.preLayout(hasBorder, cellSpacingX, cellSpacingY, widthKnown); // Increases column widths if they are less than minimums of each cell. adjustForLayoutWidths(columnSizes, hasBorder, cellSpacingX, widthKnown); // Adjust for expected total width this.adjustWidthsForExpectedMax(columnSizes, cellAvailWidth, widthKnown); } /** * This method sets the tentative actual sizes of columns (rows) based on * specified widths (heights) if available. * * @param columnSizes * @param widthsOfExtras * @param cellAvailWidth */ private static void determineTentativeSizes(final ColSizeInfo[] columnSizes, final int widthsOfExtras, final int cellAvailWidth, final boolean setNoWidthColumns) { final int numCols = columnSizes.length; // Look at percentages first int widthUsedByPercent = 0; for (int i = 0; i < numCols; i++) { final ColSizeInfo colSizeInfo = columnSizes[i]; final HtmlLength widthLength = colSizeInfo.htmlLength; if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) { final int actualSizeInt = widthLength.getLength(cellAvailWidth); widthUsedByPercent += actualSizeInt; colSizeInfo.actualSize = actualSizeInt; } } // Look at columns with absolute sizes int widthUsedByAbsolute = 0; int numNoWidthColumns = 0; for (int i = 0; i < numCols; i++) { final ColSizeInfo colSizeInfo = columnSizes[i]; final HtmlLength widthLength = colSizeInfo.htmlLength; if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) { // TODO: MULTI-LENGTH not supported final int actualSizeInt = widthLength.getRawValue(); widthUsedByAbsolute += actualSizeInt; colSizeInfo.actualSize = actualSizeInt; } else if (widthLength == null) { numNoWidthColumns++; } } // Tentative width of all columns without a declared // width is set to zero. The pre-render will determine // a better size. // // Assign all columns without widths now // int widthUsedByUnspecified = 0; // if(setNoWidthColumns) { // int remainingWidth = cellAvailWidth - widthUsedByAbsolute - // widthUsedByPercent; // if(remainingWidth > 0) { // for(int i = 0; i < numCols; i++) { // SizeInfo colSizeInfo = columnSizes[i]; // HtmlLength widthLength = colSizeInfo.htmlLength; // if(widthLength == null) { // int actualSizeInt = remainingWidth / numNoWidthColumns; // widthUsedByUnspecified += actualSizeInt; // colSizeInfo.actualSize = actualSizeInt; // } // } // } // } // Contract if necessary. This is done again later, but this is // an optimization, as it may prevent re-layout. It is only done // if all columns have some kind of declared width. if (numNoWidthColumns == 0) { int totalWidthUsed = widthUsedByPercent + widthUsedByAbsolute; int difference = totalWidthUsed - cellAvailWidth; // See if absolutes need to be contracted if (difference > 0) { if (widthUsedByAbsolute > 0) { int expectedAbsoluteWidthTotal = widthUsedByAbsolute - difference; if (expectedAbsoluteWidthTotal < 0) { expectedAbsoluteWidthTotal = 0; } final double ratio = (double) expectedAbsoluteWidthTotal / widthUsedByAbsolute; for (int i = 0; i < numCols; i++) { final ColSizeInfo sizeInfo = columnSizes[i]; final HtmlLength widthLength = columnSizes[i].htmlLength; if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) { final int oldActualSize = sizeInfo.actualSize; final int newActualSize = (int) Math.round(oldActualSize * ratio); sizeInfo.actualSize = newActualSize; totalWidthUsed += (newActualSize - oldActualSize); } } difference = totalWidthUsed - cellAvailWidth; } // See if percentages need to be contracted if (difference > 0) { if (widthUsedByPercent > 0) { int expectedPercentWidthTotal = widthUsedByPercent - difference; if (expectedPercentWidthTotal < 0) { expectedPercentWidthTotal = 0; } final double ratio = (double) expectedPercentWidthTotal / widthUsedByPercent; for (int i = 0; i < numCols; i++) { final ColSizeInfo sizeInfo = columnSizes[i]; final HtmlLength widthLength = columnSizes[i].htmlLength; if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) { final int oldActualSize = sizeInfo.actualSize; final int newActualSize = (int) Math.round(oldActualSize * ratio); sizeInfo.actualSize = newActualSize; totalWidthUsed += (newActualSize - oldActualSize); } } } } } } } /** * Expands column sizes according to layout sizes. */ private static void adjustForLayoutWidths(final ColSizeInfo[] columnSizes, final int hasBorder, final int cellSpacing, final boolean tableWidthKnown) { final int numCols = columnSizes.length; for (int i = 0; i < numCols; i++) { final ColSizeInfo si = columnSizes[i]; if (si.actualSize < si.layoutSize) { si.actualSize = si.layoutSize; } if (si.fullActualSize < si.fullLayoutSize) { si.fullActualSize = si.fullLayoutSize; } // else if(si.htmlLength == null) { // // For cells without a declared width, see if // // their tentative width is a bit too big. // if(si.actualSize > si.layoutSize) { // si.actualSize = si.layoutSize; // } // } } } private void layoutColumn(final ColSizeInfo[] columnSizes, final ColSizeInfo colSize, final int col, final int cellSpacingX, final int hasBorder) { final RowSizeInfo[] rowSizes = this.rowSizes; final ArrayList rows = this.ROWS; final int numRows = rows.size(); final int actualSize = colSize.actualSize; colSize.layoutSize = 0; for (int rowIndx = 0; rowIndx < numRows;) { // SizeInfo rowSize = rowSizes[row]; final Row row = rows.get(rowIndx); VirtualCell vc = null; try { vc = row.get(col); } catch (final IndexOutOfBoundsException iob) { vc = null; } final RAbstractCell ac = vc == null ? null : vc.getActualCell(); if (ac != null) { if (ac.getVirtualRow() == rowIndx) { // Only process actual cells with a row // beginning at the current row being processed. final int colSpan = ac.getColSpan(); if (colSpan > 1) { final int firstCol = ac.getVirtualColumn(); final int cellExtras = (colSpan - 1) * (cellSpacingX + (2 * hasBorder)); int vcActualWidth = cellExtras; for (int x = 0; x < colSpan; x++) { vcActualWidth += columnSizes[firstCol + x].actualSize; } // TODO: better height possible final Dimension size = ac.doCellLayout(vcActualWidth, 0, true, true, true); final int vcRenderWidth = size.width; final int denominator = (vcActualWidth - cellExtras); int newTentativeCellWidth; if (denominator > 0) { newTentativeCellWidth = (actualSize * (vcRenderWidth - cellExtras)) / denominator; } else { newTentativeCellWidth = (vcRenderWidth - cellExtras) / colSpan; } if (newTentativeCellWidth > colSize.layoutSize) { colSize.layoutSize = newTentativeCellWidth; } final int rowSpan = ac.getRowSpan(); final int vch = (size.height - ((rowSpan - 1) * (this.cellSpacingY + (2 * hasBorder)))) / rowSpan; for (int y = 0; y < rowSpan; y++) { if (rowSizes[rowIndx + y].minSize < vch) { rowSizes[rowIndx + y].minSize = vch; } } } else { // TODO: better height possible final Dimension size = ac.doCellLayout(actualSize, 0, true, true, true); if (size.width > colSize.layoutSize) { colSize.layoutSize = size.width; } @NonNull Insets cbi = ac.getBorderInsets(); final int cellFullLayoutWidth = size.width + cbi.left + cbi.right; if (cellFullLayoutWidth > colSize.fullLayoutSize) { colSize.fullLayoutSize = cellFullLayoutWidth; } final int rowSpan = ac.getRowSpan(); final int vch = (size.height - ((rowSpan - 1) * (this.cellSpacingY + (2 * hasBorder)))) / rowSpan; for (int y = 0; y < rowSpan; y++) { if (rowSizes[rowIndx + y].minSize < vch) { rowSizes[rowIndx + y].minSize = vch; } } } } } // row = (ac == null ? row + 1 : ac.getVirtualRow() + ac.getRowSpan()); rowIndx++; } } private int adjustWidthsForExpectedMax(final ColSizeInfo[] columnSizes, final int cellAvailWidth, final boolean expand) { final int hasBorder = this.hasOldStyleBorder; final int cellSpacingX = this.cellSpacingX; int currentTotal = 0; final int numCols = columnSizes.length; for (int i = 0; i < numCols; i++) { currentTotal += columnSizes[i].fullActualSize; } int difference = currentTotal - (this.widthsOfExtras + cellAvailWidth); // int difference = currentTotal - (cellAvailWidth); if ((difference > 0) || ((difference < 0) && expand)) { // First, try to contract/expand columns with no width int noWidthTotal = 0; int numNoWidth = 0; for (int i = 0; i < numCols; i++) { if (columnSizes[i].htmlLength == null) { numNoWidth++; noWidthTotal += columnSizes[i].fullActualSize; } } if (numNoWidth > 0) { // TODO: This is not shrinking correctly. int expectedNoWidthTotal = noWidthTotal - difference - this.widthsOfExtras; if (expectedNoWidthTotal < 0) { expectedNoWidthTotal = 0; } final double ratio = ((double) expectedNoWidthTotal) / noWidthTotal; int noWidthCount = 0; for (int i = 0; i < numCols; i++) { final ColSizeInfo sizeInfo = columnSizes[i]; if (sizeInfo.htmlLength == null) { final int oldActualSize = sizeInfo.fullActualSize; int newActualSize; if (++noWidthCount == numNoWidth) { // Last column without a width. final int currentDiff = currentTotal - cellAvailWidth; newActualSize = oldActualSize - currentDiff; if (newActualSize < 0) { newActualSize = 0; } } else { newActualSize = (int) Math.round(oldActualSize * ratio); } sizeInfo.actualSize = newActualSize; if (newActualSize < sizeInfo.fullLayoutSize) { // See if it actually fits. this.layoutColumn(columnSizes, sizeInfo, i, cellSpacingX, hasBorder); if (newActualSize < sizeInfo.layoutSize) { // Didn't fit. newActualSize = sizeInfo.layoutSize; sizeInfo.actualSize = newActualSize; } } currentTotal += (newActualSize - oldActualSize); } } difference = currentTotal - cellAvailWidth; } // See if absolutes need to be contracted if ((difference > 0) || ((difference < 0) && expand)) { int absoluteWidthTotal = 0; for (int i = 0; i < numCols; i++) { final HtmlLength widthLength = columnSizes[i].htmlLength; if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) { absoluteWidthTotal += columnSizes[i].fullActualSize; } } if (absoluteWidthTotal > 0) { int expectedAbsoluteWidthTotal = absoluteWidthTotal - difference - this.widthsOfExtras; if (expectedAbsoluteWidthTotal < 0) { expectedAbsoluteWidthTotal = 0; } final double ratio = ((double) expectedAbsoluteWidthTotal) / absoluteWidthTotal; for (int i = 0; i < numCols; i++) { final ColSizeInfo sizeInfo = columnSizes[i]; final HtmlLength widthLength = columnSizes[i].htmlLength; if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) { final int oldActualSize = sizeInfo.fullActualSize; int newActualSize = (int) Math.round(oldActualSize * ratio); sizeInfo.actualSize = newActualSize; if (newActualSize < sizeInfo.fullLayoutSize) { // See if it actually fits. this.layoutColumn(columnSizes, sizeInfo, i, cellSpacingX, hasBorder); if (newActualSize < sizeInfo.layoutSize) { // Didn't fit. newActualSize = sizeInfo.layoutSize; sizeInfo.actualSize = newActualSize; } } currentTotal += (newActualSize - oldActualSize); } } difference = currentTotal - cellAvailWidth; } // See if percentages need to be contracted if ((difference > 0) || ((difference < 0) && expand)) { int percentWidthTotal = 0; for (int i = 0; i < numCols; i++) { final HtmlLength widthLength = columnSizes[i].htmlLength; if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) { percentWidthTotal += columnSizes[i].actualSize; } } if (percentWidthTotal > 0) { int expectedPercentWidthTotal = percentWidthTotal - difference; if (expectedPercentWidthTotal < 0) { expectedPercentWidthTotal = 0; } final double ratio = (double) expectedPercentWidthTotal / percentWidthTotal; for (int i = 0; i < numCols; i++) { final ColSizeInfo sizeInfo = columnSizes[i]; final HtmlLength widthLength = columnSizes[i].htmlLength; if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) { final int oldActualSize = sizeInfo.actualSize; int newActualSize = (int) Math.round(oldActualSize * ratio); sizeInfo.actualSize = newActualSize; if (newActualSize < sizeInfo.layoutSize) { // See if it actually fits. this.layoutColumn(columnSizes, sizeInfo, i, cellSpacingX, hasBorder); if (newActualSize < sizeInfo.layoutSize) { // Didn't fit. newActualSize = sizeInfo.layoutSize; sizeInfo.actualSize = newActualSize; } } currentTotal += (newActualSize - oldActualSize); } } } } } } else { if (expand) { for (int i = 0; i < numCols; i++) { final ColSizeInfo sizeInfo = columnSizes[i]; sizeInfo.actualSize = sizeInfo.fullActualSize; } } } return currentTotal; } /** * This method renders each cell using already set actual column widths. It * sets minimum row heights based on this. */ private final void preLayout(final int hasBorder, final int cellSpacingX, final int cellSpacingY, final boolean tableWidthKnown) { // TODO: Fix for table without width that has a subtable with width=100%. // TODO: Maybe it can be addressed when NOWRAP is implemented. // TODO: Maybe it's possible to eliminate this pre-layout altogether. final ColSizeInfo[] colSizes = this.columnSizes; final RowSizeInfo[] rowSizes = this.rowSizes; // Initialize minSize in rows final int numRows = rowSizes.length; for (int i = 0; i < numRows; i++) { rowSizes[i].minSize = 0; } // Initialize layoutSize in columns final int numCols = colSizes.length; for (int i = 0; i < numCols; i++) { colSizes[i].layoutSize = 0; colSizes[i].fullLayoutSize = 0; } for (@NonNull RAbstractCell cell: this.ALL_CELLS) { final int col = cell.getVirtualColumn(); final int colSpan = cell.getColSpan(); int cellsTotalWidth; int cellsUsedWidth; boolean widthDeclared = false; if (colSpan > 1) { cellsUsedWidth = 0; for (int x = 0; x < colSpan; x++) { final ColSizeInfo colSize = colSizes[col + x]; if (colSize.htmlLength != null) { widthDeclared = true; } cellsUsedWidth += colSize.actualSize; } cellsTotalWidth = cellsUsedWidth + ((colSpan - 1) * (cellSpacingX + (2 * hasBorder))); } else { final ColSizeInfo colSize = colSizes[col]; if (colSize.htmlLength != null) { widthDeclared = true; } cellsUsedWidth = cellsTotalWidth = colSize.actualSize; } // TODO: A tentative height could be used here: Height of // table divided by number of rows. Dimension size; final RenderThreadState state = RenderThreadState.getState(); final boolean prevOverrideNoWrap = state.overrideNoWrap; try { if (!prevOverrideNoWrap) { state.overrideNoWrap = !widthDeclared; } size = cell.doCellLayout(cellsTotalWidth, 0, true, true, true); } finally { state.overrideNoWrap = prevOverrideNoWrap; } // Set render widths final int cellLayoutWidth = size.width; @NonNull Insets cbi = cell.getBorderInsets(); final int cellFullLayoutWidth = size.width + cbi.left + cbi.right; if (colSpan > 1) { // TODO: set fullLayoutSize if (cellsUsedWidth > 0) { final double ratio = (double) cellLayoutWidth / cellsUsedWidth; for (int x = 0; x < colSpan; x++) { final ColSizeInfo si = colSizes[col + x]; final int newLayoutSize = (int) Math.round(si.actualSize * ratio); if (si.layoutSize < newLayoutSize) { si.layoutSize = newLayoutSize; } } } else { final int newLayoutSize = cellLayoutWidth / colSpan; for (int x = 0; x < colSpan; x++) { final ColSizeInfo si = colSizes[col + x]; if (si.layoutSize < newLayoutSize) { si.layoutSize = newLayoutSize; } } } } else { final ColSizeInfo colSizeInfo = colSizes[col]; if (colSizeInfo.layoutSize < cellLayoutWidth) { colSizeInfo.layoutSize = cellLayoutWidth; } if (colSizeInfo.fullLayoutSize < cellFullLayoutWidth) { colSizeInfo.fullLayoutSize = cellFullLayoutWidth; } } // Set minimum heights final int actualCellHeight = size.height; final int row = cell.getVirtualRow(); final int rowSpan = cell.getRowSpan(); if (rowSpan > 1) { final int vch = (actualCellHeight - ((rowSpan - 1) * (cellSpacingY + (2 * hasBorder)))) / rowSpan; for (int y = 0; y < rowSpan; y++) { if (rowSizes[row + y].minSize < vch) { rowSizes[row + y].minSize = vch; } } } else { if (rowSizes[row].minSize < actualCellHeight) { rowSizes[row].minSize = actualCellHeight; } } } } private void determineRowSizes(final int hasBorder, final int cellSpacing, final int availHeight, final boolean sizeOnly) { final HtmlLength tableHeightLength = TableMatrix.getHeightLength(this.tableElement, availHeight); int tableHeight; final RowSizeInfo[] rowSizes = this.rowSizes; final int numRows = rowSizes.length; final int heightsOfExtras = this.heightsOfExtras; if (tableHeightLength != null) { tableHeight = tableHeightLength.getLength(availHeight); this.determineRowSizesFixedTH(hasBorder, cellSpacing, availHeight, tableHeight, sizeOnly); } else { tableHeight = heightsOfExtras; for (int row = 0; row < numRows; row++) { tableHeight += rowSizes[row].minSize; } this.determineRowSizesFlexibleTH(hasBorder, cellSpacing, availHeight, sizeOnly); } } private void determineRowSizesFixedTH(final int hasBorder, final int cellSpacing, final int availHeight, final int tableHeight, final boolean sizeOnly) { final RowSizeInfo[] rowSizes = this.rowSizes; final int numRows = rowSizes.length; final int heightsOfExtras = this.heightsOfExtras; int cellAvailHeight = tableHeight - heightsOfExtras; if (cellAvailHeight < 0) { cellAvailHeight = 0; } // Look at percentages first int heightUsedbyPercent = 0; int otherMinSize = 0; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength heightLength = rowSizeInfo.htmlLength; if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) { int actualSizeInt = heightLength.getLength(cellAvailHeight); if (actualSizeInt < rowSizeInfo.minSize) { actualSizeInt = rowSizeInfo.minSize; } heightUsedbyPercent += actualSizeInt; rowSizeInfo.actualSize = actualSizeInt; } else { otherMinSize += rowSizeInfo.minSize; } } // Check if rows with percent are bigger than they should be if ((heightUsedbyPercent + otherMinSize) > cellAvailHeight) { final double ratio = (double) (cellAvailHeight - otherMinSize) / heightUsedbyPercent; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength heightLength = rowSizeInfo.htmlLength; if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) { final int actualSize = rowSizeInfo.actualSize; final int prevActualSize = actualSize; int newActualSize = (int) Math.round(prevActualSize * ratio); if (newActualSize < rowSizeInfo.minSize) { newActualSize = rowSizeInfo.minSize; } heightUsedbyPercent += (newActualSize - prevActualSize); rowSizeInfo.actualSize = newActualSize; } } } // Look at rows with absolute sizes int heightUsedByAbsolute = 0; int noHeightMinSize = 0; int numNoHeightColumns = 0; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength heightLength = rowSizeInfo.htmlLength; if ((heightLength != null) && (heightLength.getLengthType() != HtmlLength.LENGTH)) { // TODO: MULTI-LENGTH not supported int actualSizeInt = heightLength.getRawValue(); if (actualSizeInt < rowSizeInfo.minSize) { actualSizeInt = rowSizeInfo.minSize; } heightUsedByAbsolute += actualSizeInt; rowSizeInfo.actualSize = actualSizeInt; } else if (heightLength == null) { numNoHeightColumns++; noHeightMinSize += rowSizeInfo.minSize; } } // Check if absolute sizing is too much if ((heightUsedByAbsolute + heightUsedbyPercent + noHeightMinSize) > cellAvailHeight) { final double ratio = (double) (cellAvailHeight - noHeightMinSize - heightUsedbyPercent) / heightUsedByAbsolute; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength heightLength = rowSizeInfo.htmlLength; if ((heightLength != null) && (heightLength.getLengthType() != HtmlLength.LENGTH)) { final int actualSize = rowSizeInfo.actualSize; final int prevActualSize = actualSize; int newActualSize = (int) Math.round(prevActualSize * ratio); if (newActualSize < rowSizeInfo.minSize) { newActualSize = rowSizeInfo.minSize; } heightUsedByAbsolute += (newActualSize - prevActualSize); rowSizeInfo.actualSize = newActualSize; } } } // Assign all rows without heights now final int remainingHeight = cellAvailHeight - heightUsedByAbsolute - heightUsedbyPercent; int heightUsedByRemaining = 0; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength heightLength = rowSizeInfo.htmlLength; if (heightLength == null) { int actualSizeInt = remainingHeight / numNoHeightColumns; if (actualSizeInt < rowSizeInfo.minSize) { actualSizeInt = rowSizeInfo.minSize; } heightUsedByRemaining += actualSizeInt; rowSizeInfo.actualSize = actualSizeInt; } } // Calculate actual table width final int totalUsed = heightUsedByAbsolute + heightUsedbyPercent + heightUsedByRemaining; if (totalUsed >= cellAvailHeight) { this.tableHeight = totalUsed + heightsOfExtras; } else { // Rows too short; expand them final double ratio = (double) cellAvailHeight / totalUsed; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final int actualSize = rowSizeInfo.actualSize; rowSizeInfo.actualSize = (int) Math.round(actualSize * ratio); } this.tableHeight = tableHeight; } // TODO: // This final render is probably unnecessary. Avoid exponential rendering // by setting a single height of subcell. Verify that IE only sets height // of subcells when height of row or table are specified. this.finalLayout(hasBorder, cellSpacing, sizeOnly); } private void determineRowSizesFlexibleTH(final int hasBorder, final int cellSpacing, final int availHeight, final boolean sizeOnly) { final RowSizeInfo[] rowSizes = this.rowSizes; final int numRows = rowSizes.length; final int heightsOfExtras = this.heightsOfExtras; // Look at rows with absolute sizes int heightUsedByAbsolute = 0; int percentSum = 0; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength heightLength = rowSizeInfo.htmlLength; if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.PIXELS)) { // TODO: MULTI-LENGTH not supported int actualSizeInt = heightLength.getRawValue(); if (actualSizeInt < rowSizeInfo.minSize) { actualSizeInt = rowSizeInfo.minSize; } heightUsedByAbsolute += actualSizeInt; rowSizeInfo.actualSize = actualSizeInt; } else if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) { percentSum += heightLength.getRawValue(); } } // Look at rows with no specified heights int heightUsedByNoSize = 0; // Set sizes to in row height for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength widthLength = rowSizeInfo.htmlLength; if (widthLength == null) { final int actualSizeInt = rowSizeInfo.minSize; heightUsedByNoSize += actualSizeInt; rowSizeInfo.actualSize = actualSizeInt; } } // Calculate actual total cell width final int expectedTotalCellHeight = (int) Math.round((heightUsedByAbsolute + heightUsedByNoSize) / (1 - (percentSum / 100.0))); // Set widths of columns with percentages int heightUsedByPercent = 0; for (int i = 0; i < numRows; i++) { final RowSizeInfo rowSizeInfo = rowSizes[i]; final HtmlLength heightLength = rowSizeInfo.htmlLength; if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) { int actualSizeInt = heightLength.getLength(expectedTotalCellHeight); if (actualSizeInt < rowSizeInfo.minSize) { actualSizeInt = rowSizeInfo.minSize; } heightUsedByPercent += actualSizeInt; rowSizeInfo.actualSize = actualSizeInt; } } // Set width of table this.tableHeight = heightUsedByAbsolute + heightUsedByNoSize + heightUsedByPercent + heightsOfExtras; // Do a final layouts to set actual cell sizes this.finalLayout(hasBorder, cellSpacing, sizeOnly); } /** * This method layouts each cell using already set actual column widths. It * sets minimum row heights based on this. */ private final void finalLayout(final int hasBorder, final int cellSpacing, final boolean sizeOnly) { // finalLayout needs to adjust actualSize of columns and rows // given that things might change as we layout one last time. final ColSizeInfo[] colSizes = this.columnSizes; final RowSizeInfo[] rowSizes = this.rowSizes; for (@NonNull RAbstractCell cell : this.ALL_CELLS) { final int col = cell.getVirtualColumn(); final int colSpan = cell.getColSpan(); int totalCellWidth; if (colSpan > 1) { totalCellWidth = (colSpan - 1) * (cellSpacing + (2 * hasBorder)); for (int x = 0; x < colSpan; x++) { totalCellWidth += colSizes[col + x].actualSize; } } else { totalCellWidth = colSizes[col].actualSize; } final int row = cell.getVirtualRow(); final int rowSpan = cell.getRowSpan(); int totalCellHeight; if (rowSpan > 1) { totalCellHeight = (rowSpan - 1) * (cellSpacing + (2 * hasBorder)); for (int y = 0; y < rowSpan; y++) { totalCellHeight += rowSizes[row + y].actualSize; } } else { totalCellHeight = rowSizes[row].actualSize; } final Dimension size = cell.doCellLayout(totalCellWidth, totalCellHeight, true, true, sizeOnly); if (size.width > totalCellWidth) { if (colSpan == 1) { colSizes[col].actualSize = size.width; } else { colSizes[col].actualSize += (size.width - totalCellWidth); } } if (size.height > totalCellHeight) { if (rowSpan == 1) { rowSizes[row].actualSize = size.height; } else { rowSizes[row].actualSize += (size.height - totalCellHeight); } } } } // public final void adjust() { // // finalRender needs to adjust actualSize of columns and rows // // given that things might change as we render one last time. // int hasBorder = this.hasOldStyleBorder; // int cellSpacingX = this.cellSpacingX; // int cellSpacingY = this.cellSpacingY; // ArrayList allCells = this.ALL_CELLS; // SizeInfo[] colSizes = this.columnSizes; // SizeInfo[] rowSizes = this.rowSizes; // int numCells = allCells.size(); // for(int i = 0; i < numCells; i++) { // RTableCell cell = (RTableCell) allCells.get(i); // int col = cell.getVirtualColumn(); // int colSpan = cell.getColSpan(); // int totalCellWidth; // if(colSpan > 1) { // totalCellWidth = (colSpan - 1) * (cellSpacingX + 2 * hasBorder); // for(int x = 0; x < colSpan; x++) { // totalCellWidth += colSizes[col + x].actualSize; // } // } // else { // totalCellWidth = colSizes[col].actualSize; // } // int row = cell.getVirtualRow(); // int rowSpan = cell.getRowSpan(); // int totalCellHeight; // if(rowSpan > 1) { // totalCellHeight = (rowSpan - 1) * (cellSpacingY + 2 * hasBorder); // for(int y = 0; y < rowSpan; y++) { // totalCellHeight += rowSizes[row + y].actualSize; // } // } // else { // totalCellHeight = rowSizes[row].actualSize; // } // cell.adjust(); // Dimension size = cell.getSize(); // if(size.width > totalCellWidth) { // if(colSpan == 1) { // colSizes[col].actualSize = size.width; // } // else { // colSizes[col].actualSize += (size.width - totalCellWidth); // } // } // if(size.height > totalCellHeight) { // if(rowSpan == 1) { // rowSizes[row].actualSize = size.height; // } // else { // rowSizes[row].actualSize += (size.height - totalCellHeight); // } // } // } // } /** * Sets bounds of each cell's component, and sums up table width and height. */ public final void doLayout(final Insets insets) { // Set row offsets final RowSizeInfo[] rowSizes = this.rowSizes; final int numRows = rowSizes.length; int yoffset = insets.top; final int cellSpacingY = this.cellSpacingY; final int hasBorder = this.hasOldStyleBorder; for (int i = 0; i < numRows; i++) { yoffset += cellSpacingY; yoffset += hasBorder; final RowSizeInfo rowSizeInfo = rowSizes[i]; yoffset += rowSizeInfo.marginTop; rowSizeInfo.offsetY = yoffset; rowSizeInfo.insetLeft = insets.left; rowSizeInfo.insetRight = insets.right; yoffset += rowSizeInfo.actualSize; yoffset += hasBorder; yoffset += rowSizeInfo.marginBottom; } this.tableHeight = yoffset + cellSpacingY + insets.bottom; // Set column offsets final ColSizeInfo[] colSizes = this.columnSizes; final int numColumns = colSizes.length; int xoffset = insets.left; final int cellSpacingX = this.cellSpacingX; for (int i = 0; i < numColumns; i++) { xoffset += cellSpacingX; xoffset += hasBorder; final ColSizeInfo colSizeInfo = colSizes[i]; colSizeInfo.offsetX = xoffset; xoffset += colSizeInfo.actualSize; xoffset += hasBorder; } this.tableWidth = xoffset + cellSpacingX + insets.right + (maxRowGroupRight / 2); // Set offsets of each cell for (@NonNull RAbstractCell cell : this.ALL_CELLS) { cell.setCellBounds(colSizes, rowSizes, hasBorder, cellSpacingX, cellSpacingY); } this.rowGroupSizes = prepareRowGroupSizes(); } static class RTableRowGroup extends BaseElementRenderable { public RTableRowGroup(RenderableContainer container, ModelNode modelNode, UserAgentContext ucontext, final BorderOverrider borderOverrider) { super(container, modelNode, ucontext); this.borderOverrider.copyFrom(borderOverrider); } @Override public Iterator<@NonNull ? extends Renderable> getRenderables(boolean topFirst) { return null; } @Override public RenderableSpot getLowestRenderableSpot(int x, int y) { return null; } @Override public boolean onMouseReleased(MouseEvent event, int x, int y) { return false; } @Override public boolean onMouseDisarmed(MouseEvent event) { return false; } @Override public boolean onDoubleClick(MouseEvent event, int x, int y) { return false; } @Override public void repaint() { container.repaint(x, y, width, height); } @Override public void repaint(ModelNode modelNode) { // TODO Auto-generated method stub } @Override public Color getPaintedBackgroundColor() { // TODO Auto-generated method stub return null; } @Override protected void paintShifted(Graphics g) { // TODO Auto-generated method stub } @Override protected void doLayout(int availWidth, int availHeight, boolean sizeOnly) { // TODO Auto-generated method stub } @Override public @NonNull Insets getBorderInsets() { return borderOverrider.get(super.getBorderInsets()); } } public final void paint(final Graphics g, final Dimension size) { // Paint row group backgrounds for (final RowGroupSizeInfo rgsi : rowGroupSizes) { rgsi.prePaintBackground(g); } for (final @NonNull RAbstractCell cell : this.ALL_CELLS) { // Should clip table cells, just in case. final Graphics newG = g.create(cell.x, cell.y, cell.width, cell.height); try { cell.paint(newG); } finally { newG.dispose(); } } if (this.hasOldStyleBorder > 0) { // // Paint table border // // int tableWidth = this.tableWidth; // int tableHeight = this.tableHeight; // g.setColor(Color.BLACK); //TODO: Actual border color // int x = insets.left; // int y = insets.top; // for(int i = 0; i < border; i++) { // g.drawRect(x + i, y + i, tableWidth - i * 2 - 1, tableHeight - i * 2 - // 1); // } // Paint cell borders g.setColor(Color.GRAY); for (@NonNull RAbstractCell cell : this.ALL_CELLS) { final int cx = cell.getX() - 1; final int cy = cell.getY() - 1; final int cwidth = cell.getWidth() + 1; final int cheight = cell.getHeight() + 1; g.drawRect(cx, cy, cwidth, cheight); } } // Paint row group borders for (final RowGroupSizeInfo rgsi : rowGroupSizes) { rgsi.prePaintBorder(g); } } // Called during paint private ArrayList prepareRowGroupSizes() { final ArrayList rowGroupSizes = new ArrayList<>(); { final RowSizeInfo[] rowSizesLocal = this.rowSizes; for (final RowGroup rowGroup : this.ROW_GROUPS) { if (rowGroup.rowGroupElem != null) { final Row firstRow = rowGroup.rows.get(0); final Row lastRow = rowGroup.rows.get(rowGroup.rows.size() - 1); final RowSizeInfo firstRowSize = rowSizesLocal[firstRow.rowIndex]; final RowSizeInfo lastRowSize = rowSizesLocal[lastRow.rowIndex]; final int groupHeight = lastRowSize.actualSize + lastRowSize.offsetY - (firstRowSize.offsetY); final int groupWidth = this.tableWidth - (firstRowSize.insetRight + firstRowSize.insetLeft); final RTableRowGroup rRowGroup = new RTableRowGroup(this.container, firstRow.rowGroupElem, this.uaContext, rowGroup.borderOverrider); final int x = firstRowSize.offsetX + firstRowSize.insetLeft; final int y = firstRowSize.offsetY; rRowGroup.setX(x); rRowGroup.setY(y); rRowGroup.setWidth(groupWidth); rRowGroup.setHeight(groupHeight); rRowGroup.applyStyle(groupWidth, groupHeight, true); final RowGroupSizeInfo rgsi = new RowGroupSizeInfo(groupWidth, groupHeight, rRowGroup, x, y); rowGroupSizes.add(rgsi); } } } return rowGroupSizes; } // public boolean paintSelection(Graphics g, boolean inSelection, // RenderableSpot startPoint, RenderableSpot endPoint) { // ArrayList allCells = this.ALL_CELLS; // int numCells = allCells.size(); // for(int i = 0; i < numCells; i++) { // RTableCell cell = (RTableCell) allCells.get(i); // Rectangle bounds = cell.getBounds(); // int offsetX = bounds.x; // int offsetY = bounds.y; // g.translate(offsetX, offsetY); // try { // boolean newInSelection = cell.paintSelection(g, inSelection, startPoint, // endPoint); // if(inSelection && !newInSelection) { // return false; // } // inSelection = newInSelection; // } finally { // g.translate(-offsetX, -offsetY); // } // } // return inSelection; // } // // public boolean extractSelectionText(StringBuffer buffer, boolean // inSelection, RenderableSpot startPoint, RenderableSpot endPoint) { // ArrayList allCells = this.ALL_CELLS; // int numCells = allCells.size(); // for(int i = 0; i < numCells; i++) { // RTableCell cell = (RTableCell) allCells.get(i); // boolean newInSelection = cell.extractSelectionText(buffer, inSelection, // startPoint, endPoint); // if(inSelection && !newInSelection) { // return false; // } // inSelection = newInSelection; // } // return inSelection; // } /* * (non-Javadoc) * * @see org.xamjwg.html.renderer.BoundableRenderable#getRenderablePoint(int, * int) */ public RenderableSpot getLowestRenderableSpot(final int x, final int y) { for (@NonNull RAbstractCell cell : this.ALL_CELLS) { final Rectangle bounds = cell.getVisualBounds(); if (bounds.contains(x, y)) { final RenderableSpot rp = cell.getLowestRenderableSpot(x - bounds.x, y - bounds.y); if (rp != null) { return rp; } } } return null; } /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.BoundableRenderable#onMouseClick(java.awt.event * .MouseEvent, int, int) */ public boolean onMouseClick(final MouseEvent event, final int x, final int y) { for (@NonNull RAbstractCell cell : this.ALL_CELLS) { final Rectangle bounds = cell.getVisualBounds(); if (bounds.contains(x, y)) { if (!cell.onMouseClick(event, x - bounds.x, y - bounds.y)) { return false; } break; } } return true; } public boolean onDoubleClick(final MouseEvent event, final int x, final int y) { for (@NonNull RAbstractCell cell : this.ALL_CELLS) { final Rectangle bounds = cell.getVisualBounds(); if (bounds.contains(x, y)) { if (!cell.onDoubleClick(event, x - bounds.x, y - bounds.y)) { return false; } break; } } return true; } private BoundableRenderable armedRenderable; private int maxRowGroupLeft; private int maxRowGroupRight; /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.BoundableRenderable#onMouseDisarmed(java.awt.event * .MouseEvent) */ public boolean onMouseDisarmed(final MouseEvent event) { final BoundableRenderable ar = this.armedRenderable; if (ar != null) { this.armedRenderable = null; return ar.onMouseDisarmed(event); } else { return true; } } /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.BoundableRenderable#onMousePressed(java.awt.event * .MouseEvent, int, int) */ public boolean onMousePressed(final MouseEvent event, final int x, final int y) { final ArrayList allCells = this.ALL_CELLS; final int numCells = allCells.size(); for (int i = 0; i < numCells; i++) { final RAbstractCell cell = allCells.get(i); final Rectangle bounds = cell.getVisualBounds(); if (bounds.contains(x, y)) { if (!cell.onMousePressed(event, x - bounds.x, y - bounds.y)) { this.armedRenderable = cell; return false; } break; } } return true; } /* * (non-Javadoc) * * @see * org.xamjwg.html.renderer.BoundableRenderable#onMouseReleased(java.awt.event * .MouseEvent, int, int) */ public boolean onMouseReleased(final MouseEvent event, final int x, final int y) { final ArrayList allCells = this.ALL_CELLS; final int numCells = allCells.size(); boolean found = false; for (int i = 0; i < numCells; i++) { final RAbstractCell cell = allCells.get(i); final Rectangle bounds = cell.getVisualBounds(); if (bounds.contains(x, y)) { found = true; final BoundableRenderable oldArmedRenderable = this.armedRenderable; if ((oldArmedRenderable != null) && (cell != oldArmedRenderable)) { oldArmedRenderable.onMouseDisarmed(event); this.armedRenderable = null; } if (!cell.onMouseReleased(event, x - bounds.x, y - bounds.y)) { return false; } break; } } if (!found) { final BoundableRenderable oldArmedRenderable = this.armedRenderable; if (oldArmedRenderable != null) { oldArmedRenderable.onMouseDisarmed(event); this.armedRenderable = null; } } return true; } Iterator<@NonNull RAbstractCell> getCells() { return this.ALL_CELLS.iterator(); } static final class ColSizeInfo { HtmlLength htmlLength; int actualSize; int fullActualSize; // Full size including border and padding int layoutSize; int fullLayoutSize; // Full size including border and padding int minSize; int offsetX; } static final class RowSizeInfo { int insetLeft; int insetRight; HtmlLength htmlLength; int actualSize; int minSize; int offsetX; int offsetY; int marginTop; int marginBottom; } private static final class RowGroupSizeInfo { private final int height; private final int width; private final int x; private final int y; private final @NonNull RTableRowGroup r; RowGroupSizeInfo(final int width, final int height, final @NonNull RTableRowGroup r, final int x, final int y) { this.height = height; this.width = width; this.r = r; this.x = x; this.y = y; } void prePaintBackground(final Graphics g) { final Insets bi = r.getBorderInsets(); final ModelNode rowGroupElem = r.getModelNode(); r.prePaintBackground(g, width - (bi.left/2), height, x, y, rowGroupElem, rowGroupElem.getRenderState(), bi); } void prePaintBorder(final Graphics g) { final Insets bi = r.getBorderInsets(); r.prePaintBorder(g, width + (bi.left)/2 + bi.right, height + bi.top + bi.bottom, x - bi.left, y - bi.top , bi); } } public Iterator<@NonNull RTableRowGroup> getRowGroups() { return this.rowGroupSizes.stream().map(rgs -> rgs.r).iterator(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy