1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 /* $Id: RowPainter.java 1835810 2018-07-13 10:29:57Z ssteiner $ */ 19 20 package org.apache.fop.layoutmgr.table; 21 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.List; 25 import java.util.ListIterator; 26 27 import org.apache.commons.logging.Log; 28 import org.apache.commons.logging.LogFactory; 29 30 import org.apache.fop.area.Block; 31 import org.apache.fop.area.Trait; 32 import org.apache.fop.fo.flow.table.ConditionalBorder; 33 import org.apache.fop.fo.flow.table.EffRow; 34 import org.apache.fop.fo.flow.table.EmptyGridUnit; 35 import org.apache.fop.fo.flow.table.GridUnit; 36 import org.apache.fop.fo.flow.table.PrimaryGridUnit; 37 import org.apache.fop.fo.flow.table.Table; 38 import org.apache.fop.fo.flow.table.TableColumn; 39 import org.apache.fop.fo.flow.table.TablePart; 40 import org.apache.fop.fo.properties.CommonBorderPaddingBackground; 41 import org.apache.fop.fo.properties.CommonBorderPaddingBackground.BorderInfo; 42 import org.apache.fop.layoutmgr.ElementListUtils; 43 import org.apache.fop.layoutmgr.KnuthElement; 44 import org.apache.fop.layoutmgr.KnuthPossPosIter; 45 import org.apache.fop.layoutmgr.LayoutContext; 46 import org.apache.fop.layoutmgr.SpaceResolver; 47 import org.apache.fop.layoutmgr.TraitSetter; 48 49 class RowPainter { 50 private static Log log = LogFactory.getLog(RowPainter.class); 51 private int colCount; 52 private int currentRowOffset; 53 /** Currently handled row (= last encountered row). */ 54 private EffRow currentRow; 55 private LayoutContext layoutContext; 56 /** 57 * Index of the first row of the current part present on the current page. 58 */ 59 private int firstRowIndex; 60 61 /** 62 * Index of the very first row on the current page. Needed to properly handle 63 * {@link BorderProps#COLLAPSE_OUTER}. This is not the same as {@link #firstRowIndex} 64 * when the table has headers! 65 */ 66 private int firstRowOnPageIndex; 67 68 /** 69 * Keeps track of the y-offsets of each row on a page. 70 * This is particularly needed for spanned cells where you need to know the y-offset 71 * of the starting row when the area is generated at the time the cell is closed. 72 */ 73 private List rowOffsets = new ArrayList(); 74 75 private int[] cellHeights; 76 private boolean[] firstCellOnPage; 77 private CellPart[] firstCellParts; 78 private CellPart[] lastCellParts; 79 80 /** y-offset of the current table part. */ 81 private int tablePartOffset; 82 /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */ 83 private CommonBorderPaddingBackground tablePartBackground; 84 /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */ 85 private List tablePartBackgroundAreas; 86 87 private TableContentLayoutManager tclm; 88 RowPainter(TableContentLayoutManager tclm, LayoutContext layoutContext)89 RowPainter(TableContentLayoutManager tclm, LayoutContext layoutContext) { 90 this.tclm = tclm; 91 this.layoutContext = layoutContext; 92 this.colCount = tclm.getColumns().getColumnCount(); 93 this.cellHeights = new int[colCount]; 94 this.firstCellOnPage = new boolean[colCount]; 95 this.firstCellParts = new CellPart[colCount]; 96 this.lastCellParts = new CellPart[colCount]; 97 this.firstRowIndex = -1; 98 this.firstRowOnPageIndex = -1; 99 } 100 startTablePart(TablePart tablePart)101 void startTablePart(TablePart tablePart) { 102 CommonBorderPaddingBackground background = tablePart.getCommonBorderPaddingBackground(); 103 if (background.hasBackground()) { 104 tablePartBackground = background; 105 if (tablePartBackgroundAreas == null) { 106 tablePartBackgroundAreas = new ArrayList(); 107 } 108 } 109 tablePartOffset = currentRowOffset; 110 } 111 112 /** 113 * Signals that the end of the current table part is reached. 114 * 115 * @param lastInBody true if the part is the last table-body element to be displayed 116 * on the current page. In which case all the cells must be flushed even if they 117 * aren't finished, plus the proper collapsed borders must be selected (trailing 118 * instead of normal, or rest if the cell is unfinished) 119 * @param lastOnPage true if the part is the last to be displayed on the current page. 120 * In which case collapsed after borders for the cells on the last row must be drawn 121 * in the outer mode 122 */ endTablePart(boolean lastInBody, boolean lastOnPage)123 void endTablePart(boolean lastInBody, boolean lastOnPage) { 124 addAreasAndFlushRow(lastInBody, lastOnPage); 125 126 if (tablePartBackground != null) { 127 TableLayoutManager tableLM = tclm.getTableLM(); 128 for (Object tablePartBackgroundArea : tablePartBackgroundAreas) { 129 Block backgroundArea = (Block) tablePartBackgroundArea; 130 TraitSetter.addBackground(backgroundArea, tablePartBackground, tableLM, 131 -backgroundArea.getXOffset(), tablePartOffset - backgroundArea.getYOffset(), 132 tableLM.getContentAreaIPD(), currentRowOffset - tablePartOffset); 133 } 134 tablePartBackground = null; 135 tablePartBackgroundAreas.clear(); 136 } 137 } 138 getAccumulatedBPD()139 int getAccumulatedBPD() { 140 return currentRowOffset; 141 } 142 143 /** 144 * Records the fragment of row represented by the given position. If it belongs to 145 * another (grid) row than the current one, that latter is painted and flushed first. 146 * 147 * @param tcpos a position representing the row fragment 148 */ handleTableContentPosition(TableContentPosition tcpos)149 void handleTableContentPosition(TableContentPosition tcpos) { 150 if (log.isDebugEnabled()) { 151 log.debug("===handleTableContentPosition(" + tcpos); 152 } 153 if (currentRow == null) { 154 currentRow = tcpos.getNewPageRow(); 155 } else { 156 EffRow row = tcpos.getRow(); 157 if (row.getIndex() > currentRow.getIndex()) { 158 addAreasAndFlushRow(false, false); 159 currentRow = row; 160 } 161 } 162 if (firstRowIndex < 0) { 163 firstRowIndex = currentRow.getIndex(); 164 if (firstRowOnPageIndex < 0) { 165 firstRowOnPageIndex = firstRowIndex; 166 } 167 } 168 //Iterate over all grid units in the current step 169 for (Object cellPart1 : tcpos.cellParts) { 170 CellPart cellPart = (CellPart) cellPart1; 171 if (log.isDebugEnabled()) { 172 log.debug(">" + cellPart); 173 } 174 int colIndex = cellPart.pgu.getColIndex(); 175 if (firstCellParts[colIndex] == null) { 176 firstCellParts[colIndex] = cellPart; 177 cellHeights[colIndex] = cellPart.getBorderPaddingBefore(firstCellOnPage[colIndex]); 178 } else { 179 assert firstCellParts[colIndex].pgu == cellPart.pgu; 180 cellHeights[colIndex] += cellPart.getConditionalBeforeContentLength(); 181 } 182 cellHeights[colIndex] += cellPart.getLength(); 183 lastCellParts[colIndex] = cellPart; 184 } 185 } 186 187 /** 188 * Creates the areas corresponding to the last row. That is, an area with background 189 * for the row, plus areas for all the cells that finish on the row (not spanning over 190 * further rows). 191 * 192 * @param lastInPart true if the row is the last from its table part to be displayed 193 * on the current page. In which case all the cells must be flushed even if they 194 * aren't finished, plus the proper collapsed borders must be selected (trailing 195 * instead of normal, or rest if the cell is unfinished) 196 * @param lastOnPage true if the row is the very last row of the table that will be 197 * displayed on the current page. In which case collapsed after borders must be drawn 198 * in the outer mode 199 */ addAreasAndFlushRow(boolean lastInPart, boolean lastOnPage)200 private void addAreasAndFlushRow(boolean lastInPart, boolean lastOnPage) { 201 if (log.isDebugEnabled()) { 202 log.debug("Remembering yoffset for row " + currentRow.getIndex() + ": " 203 + currentRowOffset); 204 } 205 recordRowOffset(currentRow.getIndex(), currentRowOffset); 206 207 // Need to compute the actual row height first 208 // and determine border behaviour for empty cells 209 boolean firstCellPart = true; 210 boolean lastCellPart = true; 211 int actualRowHeight = 0; 212 for (int i = 0; i < colCount; i++) { 213 GridUnit currentGU = currentRow.getGridUnit(i); 214 if (currentGU.isEmpty()) { 215 continue; 216 } 217 if (currentGU.getColSpanIndex() == 0 218 && (lastInPart || currentGU.isLastGridUnitRowSpan()) 219 && firstCellParts[i] != null) { 220 // TODO 221 // The last test above is a workaround for the stepping algorithm's 222 // fundamental flaw making it unable to produce the right element list for 223 // multiple breaks inside a same row group. 224 // (see http://wiki.apache.org/xmlgraphics-fop/TableLayout/KnownProblems) 225 // In some extremely rare cases (forced breaks, very small page height), a 226 // TableContentPosition produced during row delaying may end up alone on a 227 // page. It will not contain the CellPart instances for the cells starting 228 // the next row, so firstCellParts[i] will still be null for those ones. 229 int cellHeight = cellHeights[i]; 230 cellHeight += lastCellParts[i].getConditionalAfterContentLength(); 231 cellHeight += lastCellParts[i].getBorderPaddingAfter(lastInPart); 232 int cellOffset = getRowOffset(Math.max(firstCellParts[i].pgu.getRowIndex(), 233 firstRowIndex)); 234 actualRowHeight = Math.max(actualRowHeight, cellOffset + cellHeight 235 - currentRowOffset); 236 } 237 238 if (firstCellParts[i] != null && !firstCellParts[i].isFirstPart()) { 239 firstCellPart = false; 240 } 241 if (lastCellParts[i] != null && !lastCellParts[i].isLastPart()) { 242 lastCellPart = false; 243 } 244 } 245 246 // Then add areas for cells finishing on the current row 247 for (int i = 0; i < colCount; i++) { 248 GridUnit currentGU = currentRow.getGridUnit(i); 249 if (currentGU.isEmpty() && !tclm.isSeparateBorderModel()) { 250 int borderBeforeWhich; 251 if (firstCellPart) { 252 if (firstCellOnPage[i]) { 253 borderBeforeWhich = ConditionalBorder.LEADING_TRAILING; 254 } else { 255 borderBeforeWhich = ConditionalBorder.NORMAL; 256 } 257 } else { 258 borderBeforeWhich = ConditionalBorder.REST; 259 } 260 int borderAfterWhich; 261 if (lastCellPart) { 262 if (lastInPart) { 263 borderAfterWhich = ConditionalBorder.LEADING_TRAILING; 264 } else { 265 borderAfterWhich = ConditionalBorder.NORMAL; 266 } 267 } else { 268 borderAfterWhich = ConditionalBorder.REST; 269 } 270 assert (currentGU instanceof EmptyGridUnit); 271 addAreaForEmptyGridUnit((EmptyGridUnit)currentGU, 272 currentRow.getIndex(), i, 273 actualRowHeight, 274 borderBeforeWhich, borderAfterWhich, 275 lastOnPage); 276 277 firstCellOnPage[i] = false; 278 } else if (currentGU.getColSpanIndex() == 0 279 && (lastInPart || currentGU.isLastGridUnitRowSpan()) 280 && firstCellParts[i] != null) { 281 assert firstCellParts[i].pgu == currentGU.getPrimary(); 282 283 int borderBeforeWhich; 284 if (firstCellParts[i].isFirstPart()) { 285 if (firstCellOnPage[i]) { 286 borderBeforeWhich = ConditionalBorder.LEADING_TRAILING; 287 } else { 288 borderBeforeWhich = ConditionalBorder.NORMAL; 289 } 290 } else { 291 assert firstCellOnPage[i]; 292 borderBeforeWhich = ConditionalBorder.REST; 293 } 294 int borderAfterWhich; 295 if (lastCellParts[i].isLastPart()) { 296 if (lastInPart) { 297 borderAfterWhich = ConditionalBorder.LEADING_TRAILING; 298 } else { 299 borderAfterWhich = ConditionalBorder.NORMAL; 300 } 301 } else { 302 borderAfterWhich = ConditionalBorder.REST; 303 } 304 305 // when adding the areas for the TableCellLayoutManager this helps with the isLast trait 306 // if, say, the first cell of a row has content that fits in the page, but the content of 307 // the second cell does not fit this will assure that the isLast trait for the first cell 308 // will also be false 309 lastCellParts[i].pgu.getCellLM().setLastTrait(lastCellParts[i].isLastPart()); 310 addAreasForCell(firstCellParts[i].pgu, 311 firstCellParts[i].start, lastCellParts[i].end, 312 actualRowHeight, borderBeforeWhich, borderAfterWhich, 313 lastOnPage); 314 firstCellParts[i] = null; // why? what about the lastCellParts[i]? 315 Arrays.fill(firstCellOnPage, i, i + currentGU.getCell().getNumberColumnsSpanned(), 316 false); 317 } 318 } 319 currentRowOffset += actualRowHeight; 320 if (lastInPart) { 321 /* 322 * Either the end of the page is reached, then this was the last call of this 323 * method and we no longer care about currentRow; or the end of a table-part 324 * (header, footer, body) has been reached, and the next row will anyway be 325 * different from the current one, and this is unnecessary to call this method 326 * again in the first lines of handleTableContentPosition, so we may reset the 327 * following variables. 328 */ 329 currentRow = null; 330 firstRowIndex = -1; 331 rowOffsets.clear(); 332 /* 333 * The current table part has just been handled. Be it the first one or not, 334 * the header or the body, in any case the borders-before of the next row 335 * (i.e., the first row of the next part if any) must be painted in 336 * COLLAPSE_INNER mode. So the firstRowOnPageIndex indicator must be kept 337 * disabled. The following way is not the most elegant one but will be good 338 * enough. 339 */ 340 firstRowOnPageIndex = Integer.MAX_VALUE; 341 } 342 } 343 344 // TODO this is not very efficient and should probably be done another way 345 // this method is only necessary when display-align = center or after, in which case 346 // the exact content length is needed to compute the size of the empty block that will 347 // be used as padding. 348 // This should be handled automatically by a proper use of Knuth elements computeContentLength(PrimaryGridUnit pgu, int startIndex, int endIndex)349 private int computeContentLength(PrimaryGridUnit pgu, int startIndex, int endIndex) { 350 if (startIndex > endIndex) { 351 // May happen if the cell contributes no content on the current page (empty 352 // cell, in most cases) 353 return 0; 354 } else { 355 ListIterator iter = pgu.getElements().listIterator(startIndex); 356 // Skip from the content length calculation glues and penalties occurring at the 357 // beginning of the page 358 boolean nextIsBox = false; 359 while (iter.nextIndex() <= endIndex && !nextIsBox) { 360 nextIsBox = ((KnuthElement) iter.next()).isBox(); 361 } 362 int len = 0; 363 if (((KnuthElement) iter.previous()).isBox()) { 364 while (iter.nextIndex() < endIndex) { 365 KnuthElement el = (KnuthElement) iter.next(); 366 if (el.isBox() || el.isGlue()) { 367 len += el.getWidth(); 368 } 369 } 370 len += ActiveCell.getElementContentLength((KnuthElement) iter.next()); 371 } 372 return len; 373 } 374 } 375 addAreasForCell(PrimaryGridUnit pgu, int startPos, int endPos, int rowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage)376 private void addAreasForCell(PrimaryGridUnit pgu, int startPos, int endPos, 377 int rowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) { 378 /* 379 * Determine the index of the first row of this cell that will be displayed on the 380 * current page. 381 */ 382 int currentRowIndex = currentRow.getIndex(); 383 int startRowIndex; 384 int firstRowHeight; 385 if (pgu.getRowIndex() >= firstRowIndex) { 386 startRowIndex = pgu.getRowIndex(); 387 if (startRowIndex < currentRowIndex) { 388 firstRowHeight = getRowOffset(startRowIndex + 1) - getRowOffset(startRowIndex); 389 } else { 390 firstRowHeight = rowHeight; 391 } 392 } else { 393 startRowIndex = firstRowIndex; 394 firstRowHeight = 0; 395 } 396 397 /* 398 * In collapsing-border model, if the cell spans over several columns/rows then 399 * dedicated areas will be created for each grid unit to hold the corresponding 400 * borders. For that we need to know the height of each grid unit, that is of each 401 * grid row spanned over by the cell 402 */ 403 int[] spannedGridRowHeights = null; 404 if (!tclm.getTableLM().getTable().isSeparateBorderModel() && pgu.hasSpanning()) { 405 spannedGridRowHeights = new int[currentRowIndex - startRowIndex + 1]; 406 int prevOffset = getRowOffset(startRowIndex); 407 for (int i = 0; i < currentRowIndex - startRowIndex; i++) { 408 int newOffset = getRowOffset(startRowIndex + i + 1); 409 spannedGridRowHeights[i] = newOffset - prevOffset; 410 prevOffset = newOffset; 411 } 412 spannedGridRowHeights[currentRowIndex - startRowIndex] = rowHeight; 413 } 414 int cellOffset = getRowOffset(startRowIndex); 415 int cellTotalHeight = rowHeight + currentRowOffset - cellOffset; 416 if (log.isDebugEnabled()) { 417 log.debug("Creating area for cell:"); 418 log.debug(" start row: " + pgu.getRowIndex() + " " + currentRowOffset + " " 419 + cellOffset); 420 log.debug(" rowHeight=" + rowHeight + " cellTotalHeight=" + cellTotalHeight); 421 } 422 TableCellLayoutManager cellLM = pgu.getCellLM(); 423 cellLM.setXOffset(tclm.getXOffsetOfGridUnit(pgu)); 424 cellLM.setYOffset(cellOffset); 425 cellLM.setContentHeight(computeContentLength(pgu, startPos, endPos)); 426 cellLM.setTotalHeight(cellTotalHeight); 427 int prevBreak = ElementListUtils.determinePreviousBreak(pgu.getElements(), startPos); 428 if (endPos >= 0) { 429 SpaceResolver.performConditionalsNotification(pgu.getElements(), 430 startPos, endPos, prevBreak); 431 } 432 cellLM.addAreas(new KnuthPossPosIter(pgu.getElements(), startPos, endPos + 1), 433 layoutContext, spannedGridRowHeights, startRowIndex - pgu.getRowIndex(), 434 currentRowIndex - pgu.getRowIndex(), borderBeforeWhich, borderAfterWhich, 435 startRowIndex == firstRowOnPageIndex, lastOnPage, this, firstRowHeight); 436 } 437 addAreaForEmptyGridUnit(EmptyGridUnit gu, int rowIndex, int colIndex, int actualRowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage)438 private void addAreaForEmptyGridUnit(EmptyGridUnit gu, int rowIndex, int colIndex, 439 int actualRowHeight, 440 int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) { 441 442 //get effective borders 443 BorderInfo borderBefore = gu.getBorderBefore(borderBeforeWhich); 444 BorderInfo borderAfter = gu.getBorderAfter(borderAfterWhich); 445 BorderInfo borderStart = gu.getBorderStart(); 446 BorderInfo borderEnd = gu.getBorderEnd(); 447 if (borderBefore.getRetainedWidth() == 0 448 && borderAfter.getRetainedWidth() == 0 449 && borderStart.getRetainedWidth() == 0 450 && borderEnd.getRetainedWidth() == 0) { 451 return; //no borders, no area necessary 452 } 453 454 TableLayoutManager tableLM = tclm.getTableLM(); 455 Table table = tableLM.getTable(); 456 TableColumn col = tclm.getColumns().getColumn(colIndex + 1); 457 458 //position information 459 boolean firstOnPage = (rowIndex == firstRowOnPageIndex); 460 boolean inFirstColumn = (colIndex == 0); 461 boolean inLastColumn = (colIndex == table.getNumberOfColumns() - 1); 462 463 //determine the block area's size 464 int ipd = col.getColumnWidth().getValue(tableLM); 465 ipd -= (borderStart.getRetainedWidth() + borderEnd.getRetainedWidth()) / 2; 466 int bpd = actualRowHeight; 467 bpd -= (borderBefore.getRetainedWidth() + borderAfter.getRetainedWidth()) / 2; 468 469 //generate the block area 470 Block block = new Block(); 471 block.setChangeBarList(tclm.getTableLM().getFObj().getChangeBarList()); 472 block.setPositioning(Block.ABSOLUTE); 473 block.addTrait(Trait.IS_REFERENCE_AREA, Boolean.TRUE); 474 block.setIPD(ipd); 475 block.setBPD(bpd); 476 block.setXOffset(tclm.getXOffsetOfGridUnit(colIndex, 1) 477 + (borderStart.getRetainedWidth() / 2)); 478 block.setYOffset(getRowOffset(rowIndex) 479 - (borderBefore.getRetainedWidth() / 2)); 480 boolean[] outer = new boolean[] {firstOnPage, lastOnPage, inFirstColumn, 481 inLastColumn}; 482 TraitSetter.addCollapsingBorders(block, 483 borderBefore, 484 borderAfter, 485 borderStart, 486 borderEnd, outer); 487 tableLM.addChildArea(block); 488 } 489 490 /** 491 * Registers the given area, that will be used to render the part of 492 * table-header/footer/body background covered by a table-cell. If percentages are 493 * used to place the background image, the final bpd of the (fraction of) table part 494 * that will be rendered on the current page must be known. The traits can't then be 495 * set when the areas for the cell are created since at that moment this bpd is yet 496 * unknown. So they will instead be set in 497 * {@link #addAreasAndFlushRow(boolean, boolean)}. 498 * 499 * @param backgroundArea the block of the cell's dimensions that will hold the part 500 * background 501 */ registerPartBackgroundArea(Block backgroundArea)502 void registerPartBackgroundArea(Block backgroundArea) { 503 tclm.getTableLM().addBackgroundArea(backgroundArea); 504 tablePartBackgroundAreas.add(backgroundArea); 505 } 506 507 /** 508 * Records the y-offset of the row with the given index. 509 * 510 * @param rowIndex index of the row 511 * @param offset y-offset of the row on the page 512 */ recordRowOffset(int rowIndex, int offset)513 private void recordRowOffset(int rowIndex, int offset) { 514 /* 515 * In some very rare cases a row may be skipped. See for example Bugzilla #43633: 516 * in a two-column table, a row contains a row-spanning cell and a missing cell. 517 * In TableStepper#goToNextRowIfCurrentFinished this row will immediately be 518 * considered as finished, since it contains no cell ending on this row. Thus no 519 * TableContentPosition will be created for this row. Thus its index will never be 520 * recorded by the #handleTableContentPosition method. 521 * 522 * The offset of such a row is the same as the next non-empty row. It's needed 523 * to correctly offset blocks for cells starting on this row. Hence the loop 524 * below. 525 */ 526 for (int i = rowOffsets.size(); i <= rowIndex - firstRowIndex; i++) { 527 rowOffsets.add(offset); 528 } 529 } 530 531 /** 532 * Returns the offset of the row with the given index. 533 * 534 * @param rowIndex index of the row 535 * @return its y-offset on the page 536 */ getRowOffset(int rowIndex)537 private int getRowOffset(int rowIndex) { 538 return (Integer) rowOffsets.get(rowIndex - firstRowIndex); 539 } 540 541 // TODO get rid of that 542 /** Signals that the first table-body instance has started. */ startBody()543 void startBody() { 544 Arrays.fill(firstCellOnPage, true); 545 } 546 547 // TODO get rid of that 548 /** Signals that the last table-body instance has ended. */ endBody()549 void endBody() { 550 Arrays.fill(firstCellOnPage, false); 551 } 552 } 553