/*******************************************************************************
* This file is part of logisim-evolution.
*
* logisim-evolution is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* logisim-evolution is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with logisim-evolution. If not, see <http://www.gnu.org/licenses/>.
*
* Original code by Carl Burch (http://www.cburch.com), 2011.
* Subsequent modifications by :
* + Haute École Spécialisée Bernoise
* http://www.bfh.ch
* + Haute École du paysage, d'ingénierie et d'architecture de Genève
* http://hepia.hesge.ch/
* + Haute École d'Ingénierie et de Gestion du Canton de Vaud
* http://www.heig-vd.ch/
* The project is currently maintained by :
* + REDS Institute - HEIG-VD
* Yverdon-les-Bains, Switzerland
* http://reds.heig-vd.ch
*******************************************************************************/
package com.cburch.logisim.analyze.gui;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.util.ArrayList;
import javax.swing.JPanel;
import com.cburch.logisim.analyze.model.Expression;
import com.cburch.logisim.analyze.model.ExpressionVisitor;
class ExpressionView extends JPanel {
private static class ExpressionData {
String text;
final ArrayList<NotData> nots = new ArrayList<NotData>();
int[] badness;
ExpressionData(Expression expr) {
if (expr == null) {
text = "";
badness = new int[0];
} else {
computeText(expr);
computeBadnesses();
}
}
private void computeBadnesses() {
badness = new int[text.length() + 1];
badness[text.length()] = 0;
if (text.length() == 0)
return;
badness[0] = Integer.MAX_VALUE;
NotData curNot = nots.isEmpty() ? null : (NotData) nots.get(0);
int curNotIndex = 0;
char prev = text.charAt(0);
for (int i = 1; i < text.length(); i++) {
// invariant: curNot.stopIndex >= i (and is first such),
// or curNot == null if none such exists
char cur = text.charAt(i);
if (cur == ' ') {
badness[i] = BADNESS_BEFORE_SPACE;
;
} else if (Character.isJavaIdentifierPart(cur)) {
if (Character.isJavaIdentifierPart(prev)) {
badness[i] = BADNESS_IDENT_BREAK;
} else {
badness[i] = BADNESS_BEFORE_AND;
}
} else if (cur == '+') {
badness[i] = BADNESS_BEFORE_OR;
} else if (cur == '^') {
badness[i] = BADNESS_BEFORE_XOR;
} else if (cur == ')') {
badness[i] = BADNESS_BEFORE_SPACE;
} else { // cur == '('
badness[i] = BADNESS_BEFORE_AND;
}
while (curNot != null && curNot.stopIndex <= i) {
++curNotIndex;
curNot = (curNotIndex >= nots.size() ? null
: (NotData) nots.get(curNotIndex));
}
if (curNot != null && badness[i] < BADNESS_IDENT_BREAK) {
int depth = 0;
NotData nd = curNot;
int ndi = curNotIndex;
while (nd != null && nd.startIndex < i) {
if (nd.stopIndex > i)
++depth;
++ndi;
nd = ndi < nots.size() ? (NotData) nots.get(ndi) : null;
}
if (depth > 0) {
badness[i] += BADNESS_NOT_BREAK + (depth - 1)
* BADNESS_PER_NOT_BREAK;
}
}
prev = cur;
}
}
private void computeText(Expression expr) {
final StringBuilder text = new StringBuilder();
expr.visit(new ExpressionVisitor<Object>() {
private Object binary(Expression a, Expression b, int level,
String op) {
if (a.getPrecedence() < level) {
text.append("(");
a.visit(this);
text.append(")");
} else {
a.visit(this);
}
text.append(op);
if (b.getPrecedence() < level) {
text.append("(");
b.visit(this);
text.append(")");
} else {
b.visit(this);
}
return null;
}
public Object visitAnd(Expression a, Expression b) {
return binary(a, b, Expression.AND_LEVEL, " ");
}
public Object visitConstant(int value) {
text.append("" + Integer.toString(value, 16));
return null;
}
public Object visitNot(Expression a) {
NotData notData = new NotData();
notData.startIndex = text.length();
nots.add(notData);
a.visit(this);
notData.stopIndex = text.length();
return null;
}
public Object visitOr(Expression a, Expression b) {
return binary(a, b, Expression.OR_LEVEL, " + ");
}
public Object visitVariable(String name) {
text.append(name);
return null;
}
public Object visitXor(Expression a, Expression b) {
return binary(a, b, Expression.XOR_LEVEL, " ^ ");
}
});
this.text = text.toString();
}
}
private class MyListener implements ComponentListener {
public void componentHidden(ComponentEvent arg0) {
}
public void componentMoved(ComponentEvent arg0) {
}
public void componentResized(ComponentEvent arg0) {
int width = getWidth();
if (renderData != null && Math.abs(renderData.width - width) > 2) {
Graphics g = getGraphics();
FontMetrics fm = g == null ? null : g.getFontMetrics();
renderData = new RenderData(renderData.exprData, width, fm);
setPreferredSize(renderData.getPreferredSize());
revalidate();
repaint();
}
}
public void componentShown(ComponentEvent arg0) {
}
}
private static class NotData {
int startIndex;
int stopIndex;
int depth;
}
private static class RenderData {
ExpressionData exprData;
int prefWidth;
int width;
int height;
String[] lineText;
ArrayList<ArrayList<NotData>> lineNots;
int[] lineY;
RenderData(ExpressionData exprData, int width, FontMetrics fm) {
this.exprData = exprData;
this.width = width;
height = MINIMUM_HEIGHT;
if (fm == null) {
lineText = new String[] { exprData.text };
lineNots = new ArrayList<ArrayList<NotData>>();
lineNots.add(exprData.nots);
computeNotDepths();
lineY = new int[] { MINIMUM_HEIGHT };
} else {
if (exprData.text.length() == 0) {
lineText = new String[] { Strings.get("expressionEmpty") };
lineNots = new ArrayList<ArrayList<NotData>>();
lineNots.add(new ArrayList<NotData>());
} else {
computeLineText(fm);
computeLineNots();
computeNotDepths();
}
computeLineY(fm);
prefWidth = lineText.length > 1 ? width : fm
.stringWidth(lineText[0]);
}
}
private void computeLineNots() {
ArrayList<NotData> allNots = exprData.nots;
lineNots = new ArrayList<ArrayList<NotData>>();
for (int i = 0; i < lineText.length; i++) {
lineNots.add(new ArrayList<NotData>());
}
for (NotData nd : allNots) {
int pos = 0;
for (int j = 0; j < lineNots.size() && pos < nd.stopIndex; j++) {
String line = lineText[j];
int nextPos = pos + line.length();
if (nextPos > nd.startIndex) {
NotData toAdd = new NotData();
toAdd.startIndex = Math.max(pos, nd.startIndex) - pos;
toAdd.stopIndex = Math.min(nextPos, nd.stopIndex) - pos;
lineNots.get(j).add(toAdd);
}
pos = nextPos;
}
}
}
private void computeLineText(FontMetrics fm) {
String text = exprData.text;
int[] badness = exprData.badness;
if (fm.stringWidth(text) <= width) {
lineText = new String[] { text };
return;
}
int startPos = 0;
ArrayList<String> lines = new ArrayList<String>();
while (startPos < text.length()) {
int stopPos = startPos + 1;
String bestLine = text.substring(startPos, stopPos);
if (stopPos >= text.length()) {
lines.add(bestLine);
break;
}
int bestStopPos = stopPos;
int lineWidth = fm.stringWidth(bestLine);
int bestBadness = badness[stopPos] + (width - lineWidth)
* BADNESS_PER_PIXEL;
while (stopPos < text.length()) {
++stopPos;
String line = text.substring(startPos, stopPos);
lineWidth = fm.stringWidth(line);
if (lineWidth > width)
break;
int lineBadness = badness[stopPos] + (width - lineWidth)
* BADNESS_PER_PIXEL;
if (lineBadness < bestBadness) {
bestBadness = lineBadness;
bestStopPos = stopPos;
bestLine = line;
}
}
lines.add(bestLine);
startPos = bestStopPos;
}
lineText = lines.toArray(new String[lines.size()]);
}
private void computeLineY(FontMetrics fm) {
lineY = new int[lineNots.size()];
int curY = 0;
for (int i = 0; i < lineY.length; i++) {
int maxDepth = -1;
ArrayList<NotData> nots = lineNots.get(i);
for (NotData nd : nots) {
if (nd.depth > maxDepth)
maxDepth = nd.depth;
}
lineY[i] = curY + maxDepth * NOT_SEP;
curY = lineY[i] + fm.getHeight() + EXTRA_LEADING;
}
height = Math.max(MINIMUM_HEIGHT, curY - fm.getLeading()
- EXTRA_LEADING);
}
private void computeNotDepths() {
for (ArrayList<NotData> nots : lineNots) {
int n = nots.size();
int[] stack = new int[n];
for (int i = 0; i < nots.size(); i++) {
NotData nd = nots.get(i);
int depth = 0;
int top = 0;
stack[0] = nd.stopIndex;
for (int j = i + 1; j < nots.size(); j++) {
NotData nd2 = nots.get(j);
if (nd2.startIndex >= nd.stopIndex)
break;
while (nd2.startIndex >= stack[top])
top--;
++top;
stack[top] = nd2.stopIndex;
if (top > depth)
depth = top;
}
nd.depth = depth;
}
}
}
public Dimension getPreferredSize() {
return new Dimension(10, height);
}
public void paint(Graphics g, int x, int y) {
FontMetrics fm = g.getFontMetrics();
int i = -1;
for (String line : lineText) {
i++;
g.drawString(line, x, y + lineY[i] + fm.getAscent());
ArrayList<NotData> nots = lineNots.get(i);
for (NotData nd : nots) {
int notY = y + lineY[i] - nd.depth * NOT_SEP;
int startX = x
+ fm.stringWidth(line.substring(0, nd.startIndex));
int stopX = x
+ fm.stringWidth(line.substring(0, nd.stopIndex));
g.drawLine(startX, notY, stopX, notY);
}
}
}
}
private static final long serialVersionUID = 1L;
private static final int BADNESS_IDENT_BREAK = 10000;
private static final int BADNESS_BEFORE_SPACE = 500;
private static final int BADNESS_BEFORE_AND = 50;
private static final int BADNESS_BEFORE_XOR = 30;
private static final int BADNESS_BEFORE_OR = 0;
private static final int BADNESS_NOT_BREAK = 100;
private static final int BADNESS_PER_NOT_BREAK = 30;
private static final int BADNESS_PER_PIXEL = 1;
private static final int NOT_SEP = 3;
private static final int EXTRA_LEADING = 4;
private static final int MINIMUM_HEIGHT = 25;
private MyListener myListener = new MyListener();
private RenderData renderData;
public ExpressionView() {
addComponentListener(myListener);
setExpression(null);
}
void localeChanged() {
repaint();
}
@Override
public void paintComponent(Graphics g) {
/* Anti-aliasing changes from https://github.com/hausen/logisim-evolution */
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(
RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
super.paintComponent(g);
if (renderData != null) {
int x = Math.max(0, (getWidth() - renderData.prefWidth) / 2);
int y = Math.max(0, (getHeight() - renderData.height) / 2);
renderData.paint(g, x, y);
}
}
public void setExpression(Expression expr) {
ExpressionData exprData = new ExpressionData(expr);
Graphics g = getGraphics();
FontMetrics fm = g == null ? null : g.getFontMetrics();
renderData = new RenderData(exprData, getWidth(), fm);
setPreferredSize(renderData.getPreferredSize());
revalidate();
repaint();
}
}