blob: 151f9ac117a540c5cdf0a7876a9c4dd769a42649 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk.skin;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.wtk.Bounds;
import org.apache.pivot.wtk.Dimensions;
import org.apache.pivot.wtk.HorizontalAlignment;
import org.apache.pivot.wtk.Platform;
import org.apache.pivot.wtk.TextPane;
import org.apache.pivot.wtk.text.Paragraph;
class TextPaneSkinParagraphView extends TextPaneSkinBlockView {
private static final int PARAGRAPH_TERMINATOR_WIDTH = 4;
private static class Row {
public int x = 0;
public int y = 0;
public int width = 0;
public int height = 0;
public ArrayList<RowSegment> rowSegments = new ArrayList<>();
}
private static class RowSegment {
public TextPaneSkinNodeView nodeView;
public int offset;
public RowSegment(TextPaneSkinNodeView nodeView, int offset) {
this.nodeView = nodeView;
this.offset = offset;
}
}
private ArrayList<Row> rows = null;
private Bounds terminatorBounds = new Bounds(0, 0, 0, 0);
public TextPaneSkinParagraphView(TextPaneSkin textPaneSkin, Paragraph paragraph) {
super(textPaneSkin, paragraph);
}
@Override
public void invalidateUpTree() {
super.invalidateUpTree();
terminatorBounds = null;
}
@Override
protected void childLayout(int breakWidth) {
// Break the views into multiple rows
Paragraph paragraph = (Paragraph) getNode();
rows = new ArrayList<>();
int offset = 0;
ParagraphChildLayouter layouter = new ParagraphChildLayouter();
Row row = new Row();
for (TextPaneSkinNodeView nodeView : this) {
nodeView.layout(Math.max(breakWidth - (row.width + PARAGRAPH_TERMINATOR_WIDTH), 0));
int nodeViewWidth = nodeView.getWidth();
if (row.width + nodeViewWidth > breakWidth && row.width > 0) {
// The view is too big to fit in the remaining space,
// and it is not the only view in this row
rows.add(row);
layouter.endRow(row);
row = new Row();
row.width = 0;
}
// Add the view to the row
RowSegment segment = new RowSegment(nodeView, offset);
row.rowSegments.add(segment);
layouter.startSegment(segment);
offset += nodeView.getCharacterCount();
row.width += nodeViewWidth;
// If the view was split into multiple views, add them to their own rows
nodeView = getNext(nodeView);
while (nodeView != null) {
rows.add(row);
layouter.endRow(row);
row = new Row();
nodeView.layout(breakWidth);
segment = new RowSegment(nodeView, offset);
row.rowSegments.add(segment);
layouter.startSegment(segment);
offset += nodeView.getCharacterCount();
row.width = nodeView.getWidth();
nodeView = getNext(nodeView);
}
}
// Add the last row
if (row.rowSegments.getLength() > 0) {
rows.add(row);
layouter.endRow(row);
}
// calculate paragraph width and adjust for alignment
layouter.end(paragraph, rows);
// Recalculate terminator bounds
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
LineMetrics lm = getTextPaneSkin().getFont().getLineMetrics("", 0, 0, fontRenderContext);
int terminatorHeight = (int) Math.ceil(lm.getHeight());
int terminatorY;
if (getCharacterCount() == 1) {
// The terminator is the only character in this paragraph
terminatorY = 0;
} else {
terminatorY = layouter.rowY - terminatorHeight;
}
terminatorBounds = new Bounds(layouter.x, terminatorY, PARAGRAPH_TERMINATOR_WIDTH,
terminatorHeight);
// Ensure that the paragraph is visible even when empty
layouter.paragraphWidth += terminatorBounds.width;
int height = Math.max(layouter.rowY, terminatorBounds.height);
setSize(layouter.paragraphWidth, height);
}
@Override
public Dimensions getPreferredSize(int breakWidth) {
// Break the views into multiple rows
Paragraph paragraph = (Paragraph) getNode();
ArrayList<Row> rowsLocal = new ArrayList<>();
int offset = 0;
ParagraphChildLayouter layouter = new ParagraphChildLayouter();
Row row = new Row();
for (TextPaneSkinNodeView nodeView : this) {
nodeView.layout(Math.max(breakWidth - (row.width + PARAGRAPH_TERMINATOR_WIDTH), 0));
int nodeViewWidth = nodeView.getWidth();
if (row.width + nodeViewWidth > breakWidth && row.width > 0) {
// The view is too big to fit in the remaining space,
// and it is not the only view in this row
rowsLocal.add(row);
layouter.endRow(row);
row = new Row();
row.width = 0;
}
// Add the view to the row
RowSegment segment = new RowSegment(nodeView, offset);
row.rowSegments.add(segment);
layouter.startSegment(segment);
offset += nodeView.getCharacterCount();
row.width += nodeViewWidth;
// If the view was split into multiple views, add them to their own rows
nodeView = getNext(nodeView);
while (nodeView != null) {
rowsLocal.add(row);
layouter.endRow(row);
row = new Row();
nodeView.layout(breakWidth);
segment = new RowSegment(nodeView, offset);
row.rowSegments.add(segment);
layouter.startSegment(segment);
offset += nodeView.getCharacterCount();
row.width = nodeView.getWidth();
nodeView = getNext(nodeView);
}
}
// Add the last row
if (row.rowSegments.getLength() > 0) {
rowsLocal.add(row);
layouter.endRow(row);
}
// calculate paragraph width and adjust for alignment
layouter.end(paragraph, rowsLocal);
// Recalculate terminator bounds
FontRenderContext fontRenderContext = Platform.getFontRenderContext();
LineMetrics lm = getTextPaneSkin().getFont().getLineMetrics("", 0, 0, fontRenderContext);
int terminatorHeight = (int) Math.ceil(lm.getHeight());
int terminatorY;
if (getCharacterCount() == 1) {
// The terminator is the only character in this paragraph
terminatorY = 0;
} else {
terminatorY = layouter.rowY - terminatorHeight;
}
Bounds terminatorBoundsLocal = new Bounds(layouter.x, terminatorY,
PARAGRAPH_TERMINATOR_WIDTH, terminatorHeight);
// Ensure that the paragraph is visible even when empty
layouter.paragraphWidth += terminatorBoundsLocal.width;
int height = Math.max(layouter.rowY, terminatorBoundsLocal.height);
return new Dimensions(layouter.paragraphWidth, height);
}
private static TextPaneSkinNodeView getNext(TextPaneSkinNodeView child) {
// Using instanceof checks because there is no nice place in the hierarchy
// to put an abstract method
if (child instanceof TextPaneSkinSpanView) {
return ((TextPaneSkinSpanView) child).getNext();
} else if (child instanceof TextPaneSkinTextNodeView) {
return ((TextPaneSkinTextNodeView) child).getNext();
} else {
throw new IllegalStateException();
}
}
/**
* The layout process and the line breaking process are interleaved, because
* tab stops mean that we need to calculate X and width for each segment
* before moving on to the next segment.
*/
private static class ParagraphChildLayouter {
// Add the row views to this view, lay out, and calculate height
public int x = 0;
public int paragraphWidth = 0;
public int rowY = 0;
public void startSegment(RowSegment segment) {
segment.nodeView.setLocation(x, segment.nodeView.getY());
x += segment.nodeView.getWidth();
}
public void endRow(Row row) {
row.y = rowY;
// Determine the row height
for (RowSegment segment : row.rowSegments) {
row.height = Math.max(row.height, segment.nodeView.getHeight());
}
// Determine row-segment Y values
int rowBaseline = -1;
for (RowSegment segment : row.rowSegments) {
rowBaseline = Math.max(rowBaseline, segment.nodeView.getBaseline());
}
for (RowSegment segment : row.rowSegments) {
int nodeViewBaseline = segment.nodeView.getBaseline();
int y;
if (rowBaseline == -1 || nodeViewBaseline == -1) {
// Align to bottom
y = row.height - segment.nodeView.getHeight();
} else {
// Align to baseline
y = rowBaseline - nodeViewBaseline;
}
segment.nodeView.setLocation(segment.nodeView.getX(), y + rowY);
}
rowY += row.height;
}
public void end(Paragraph paragraph, ArrayList<Row> rows) {
// calculate paragraph width
for (Row row : rows) {
paragraphWidth = Math.max(paragraphWidth, row.width);
}
// adjust for horizontal alignment
for (Row row : rows) {
if (paragraph.getHorizontalAlignment() == HorizontalAlignment.LEFT) {
x = 0;
} else if (paragraph.getHorizontalAlignment() == HorizontalAlignment.CENTER) {
x = (paragraphWidth - row.width) / 2;
} else if (paragraph.getHorizontalAlignment() == HorizontalAlignment.RIGHT) {
// right alignment
x = paragraphWidth - row.width;
} else {
throw new IllegalStateException();
}
for (RowSegment segment : row.rowSegments) {
segment.nodeView.setLocation(x, segment.nodeView.getY());
x += segment.nodeView.getWidth();
}
}
}
}
@Override
protected void setSkinLocation(int skinX, int skinY) {
super.setSkinLocation(skinX, skinY);
for (int i = 0, n = rows.getLength(); i < n; i++) {
Row row = rows.get(i);
for (RowSegment segment : row.rowSegments) {
segment.nodeView.setSkinLocation(skinX + segment.nodeView.getX(), skinY
+ segment.nodeView.getY());
}
}
}
@Override
public void paint(Graphics2D graphics) {
// The default paint() method paints the document children, but because of row-splitting,
// the children we want to paint are not the same.
// Determine the paint bounds
Bounds paintBounds = new Bounds(0, 0, getWidth(), getHeight());
Rectangle clipBounds = graphics.getClipBounds();
if (clipBounds != null) {
paintBounds = paintBounds.intersect(clipBounds);
}
for (int i = 0, n = rows.getLength(); i < n; i++) {
Row row = rows.get(i);
for (RowSegment segment : row.rowSegments) {
paintChild(graphics, paintBounds, segment.nodeView);
}
}
}
@Override
public int getInsertionPoint(int x, int y) {
int offset = -1;
int n = rows.getLength();
if (n > 0) {
for (int i = 0; i < n; i++) {
Row row = rows.get(i);
if (y >= row.y && y < row.y + row.height) {
if (x < row.x) {
RowSegment firstNodeSegment = row.rowSegments.get(0);
offset = firstNodeSegment.offset;
} else if (x > row.x + row.width - 1) {
RowSegment lastNodeSegment = row.rowSegments.get(row.rowSegments.getLength() - 1);
offset = lastNodeSegment.offset
+ lastNodeSegment.nodeView.getCharacterCount();
if (offset < getCharacterCount() - 1) {
offset--;
}
} else {
for (RowSegment segment : row.rowSegments) {
Bounds nodeViewBounds = segment.nodeView.getBounds();
if (nodeViewBounds.contains(x, y)) {
offset = segment.nodeView.getInsertionPoint(
x - segment.nodeView.getX(), y - segment.nodeView.getY())
+ segment.offset;
break;
}
}
}
}
if (offset != -1) {
break;
}
}
} else {
offset = getCharacterCount() - 1;
}
return offset;
}
@Override
public int getNextInsertionPoint(int x, int from, TextPane.ScrollDirection direction) {
int offset = -1;
int n = rows.getLength();
if (n == 0 && from == -1) {
// There are no rows; select the terminator character
offset = 0;
} else {
int i;
if (from == -1) {
i = (direction == TextPane.ScrollDirection.DOWN) ? -1 : rows.getLength();
} else {
// Find the row that contains offset
if (from == getCharacterCount() - 1) {
i = rows.getLength() - 1;
} else {
i = 0;
while (i < n) {
Row row = rows.get(i);
RowSegment firstNodeSegment = row.rowSegments.get(0);
RowSegment lastNodeSegment = row.rowSegments.get(row.rowSegments.getLength() - 1);
if (from >= firstNodeSegment.offset
&& from < lastNodeSegment.offset
+ lastNodeSegment.nodeView.getCharacterCount()) {
break;
}
i++;
}
}
}
// Move to the next or previous row
if (direction == TextPane.ScrollDirection.DOWN) {
i++;
} else {
i--;
}
if (i >= 0 && i < n) {
// Find the node view that contains x and get the insertion point from it
Row row = rows.get(i);
for (RowSegment segment : row.rowSegments) {
Bounds bounds = segment.nodeView.getBounds();
if (x >= bounds.x && x < bounds.x + bounds.width) {
offset = segment.nodeView.getNextInsertionPoint(
x - segment.nodeView.getX(), -1, direction) + segment.offset;
break;
}
}
if (offset == -1) {
// No node view contained the x position; move to the end of the row
RowSegment lastNodeSegment = row.rowSegments.get(row.rowSegments.getLength() - 1);
offset = lastNodeSegment.offset + lastNodeSegment.nodeView.getCharacterCount();
if (offset < getCharacterCount() - 1) {
offset--;
}
}
}
}
return offset;
}
@Override
public int getRowAt(int offset) {
int rowIndex = -1;
if (offset == getCharacterCount() - 1) {
rowIndex = (rows.getLength() == 0) ? 0 : rows.getLength() - 1;
} else {
for (int i = 0, n = rows.getLength(); i < n; i++) {
Row row = rows.get(i);
RowSegment firstNodeSegment = row.rowSegments.get(0);
RowSegment lastNodeSegment = row.rowSegments.get(row.rowSegments.getLength() - 1);
if (offset >= firstNodeSegment.offset
&& offset < lastNodeSegment.offset
+ lastNodeSegment.nodeView.getCharacterCount()) {
rowIndex = i;
break;
}
}
}
return rowIndex;
}
@Override
public int getRowCount() {
return Math.max(rows.getLength(), 1);
}
@Override
public Bounds getCharacterBounds(int offset) {
Bounds characterBounds = null;
if (offset >= getCharacterCount() - 1) {
characterBounds = terminatorBounds;
} else {
if (rows != null) {
for (int i = 0, n = rows.getLength(); i < n; i++) {
Row row = rows.get(i);
for (RowSegment segment : row.rowSegments) {
int nodeViewOffset = segment.offset;
int characterCount = segment.nodeView.getCharacterCount();
if (offset >= nodeViewOffset && offset < nodeViewOffset + characterCount) {
characterBounds = segment.nodeView.getCharacterBounds(offset - nodeViewOffset);
if (characterBounds != null) {
characterBounds = characterBounds.translate(
segment.nodeView.getX(), segment.nodeView.getY());
}
break;
}
}
}
}
if (characterBounds != null) {
characterBounds = characterBounds.intersect(0, 0, getWidth(), getHeight());
}
}
return characterBounds;
}
}