mirror of
https://github.com/hneemann/Digital.git
synced 2025-09-15 15:58:41 -04:00
added test vectors. Up to now not functional.
This commit is contained in:
parent
9bac4ab980
commit
766f14dc37
@ -14,7 +14,13 @@ public class Key<VALUE> {
|
||||
private final String name;
|
||||
private final String description;
|
||||
|
||||
Key(String key, VALUE def) {
|
||||
/**
|
||||
* Creates a new Key
|
||||
*
|
||||
* @param key the key
|
||||
* @param def the default value
|
||||
*/
|
||||
public Key(String key, VALUE def) {
|
||||
this.key = key;
|
||||
String langName = "key_" + key.replace(" ", "");
|
||||
this.name = Lang.get(langName);
|
||||
|
@ -17,6 +17,7 @@ import de.neemann.digital.draw.graphics.Vector;
|
||||
import de.neemann.digital.draw.shapes.Drawable;
|
||||
import de.neemann.digital.draw.shapes.ShapeFactory;
|
||||
import de.neemann.digital.gui.components.AttributeDialog;
|
||||
import de.neemann.digital.gui.components.test.TestData;
|
||||
import de.neemann.digital.lang.Lang;
|
||||
import de.neemann.gui.language.Language;
|
||||
|
||||
@ -70,6 +71,7 @@ public class Circuit {
|
||||
xStream.addImplicitCollection(ElementAttributes.class, "attributes");
|
||||
xStream.alias("data", DataField.class);
|
||||
xStream.addImplicitCollection(DataField.class, "data");
|
||||
xStream.alias("testData", TestData.class);
|
||||
xStream.ignoreUnknownElements();
|
||||
return xStream;
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import de.neemann.digital.core.wiring.*;
|
||||
import de.neemann.digital.draw.elements.Tunnel;
|
||||
import de.neemann.digital.gui.components.data.DummyElement;
|
||||
import de.neemann.digital.gui.components.terminal.Terminal;
|
||||
import de.neemann.digital.gui.components.test.TestCaseElement;
|
||||
import de.neemann.digital.lang.Lang;
|
||||
|
||||
import java.io.File;
|
||||
@ -90,6 +91,9 @@ public class ElementLibrary implements Iterable<ElementLibrary.ElementContainer>
|
||||
add(Mul.DESCRIPTION, menu);
|
||||
add(Comparator.DESCRIPTION, menu);
|
||||
add(Neg.DESCRIPTION, menu);
|
||||
|
||||
menu = Lang.get("lib_test");
|
||||
add(TestCaseElement.TESTCASEDESCRIPTION, menu);
|
||||
}
|
||||
|
||||
private void add(ElementTypeDescription description, String treePath) {
|
||||
|
@ -18,6 +18,7 @@ import de.neemann.digital.draw.shapes.ieee.IEEEOrShape;
|
||||
import de.neemann.digital.draw.shapes.ieee.IEEEXOrShape;
|
||||
import de.neemann.digital.gui.LibrarySelector;
|
||||
import de.neemann.digital.gui.components.data.DummyElement;
|
||||
import de.neemann.digital.gui.components.test.TestCaseElement;
|
||||
import de.neemann.digital.lang.Lang;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -96,6 +97,7 @@ public final class ShapeFactory {
|
||||
map.put(Tunnel.DESCRIPTION.getName(), TunnelShape::new);
|
||||
|
||||
map.put(DummyElement.TEXTDESCRIPTION.getName(), TextShape::new);
|
||||
map.put(TestCaseElement.TESTCASEDESCRIPTION.getName(), TestCaseShape::new);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,6 +141,7 @@ public final class ShapeFactory {
|
||||
pt.getOutputDescriptions(elementAttributes));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return new MissingShape(elementName, e);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
package de.neemann.digital.draw.shapes;
|
||||
|
||||
import de.neemann.digital.core.Observer;
|
||||
import de.neemann.digital.core.element.ElementAttributes;
|
||||
import de.neemann.digital.core.element.PinDescriptions;
|
||||
import de.neemann.digital.draw.elements.IOState;
|
||||
import de.neemann.digital.draw.elements.Pins;
|
||||
import de.neemann.digital.draw.graphics.*;
|
||||
import de.neemann.digital.draw.graphics.Polygon;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
import static de.neemann.digital.draw.shapes.GenericShape.SIZE;
|
||||
import static de.neemann.digital.draw.shapes.GenericShape.SIZE2;
|
||||
|
||||
/**
|
||||
* The shape to visualize a test case
|
||||
*
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestCaseShape implements Shape {
|
||||
|
||||
private static final Style TESTSTYLE = new Style(0, true, new Color(0, 255, 0, 40));
|
||||
private final String label;
|
||||
|
||||
/**
|
||||
* Creates a new instance
|
||||
*
|
||||
* @param attributes the attributes
|
||||
* @param inputs inputs
|
||||
* @param outputs ans autputs
|
||||
*/
|
||||
public TestCaseShape(ElementAttributes attributes, PinDescriptions inputs, PinDescriptions outputs) {
|
||||
label = attributes.getCleanLabel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pins getPins() {
|
||||
return new Pins();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractorInterface applyStateMonitor(IOState ioState, Observer guiObserver) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawTo(Graphic graphic, boolean highLight) {
|
||||
Polygon pol = new Polygon(true)
|
||||
.add(SIZE2, SIZE2)
|
||||
.add(SIZE2 + SIZE * 4, SIZE2)
|
||||
.add(SIZE2 + SIZE * 4, SIZE * 2 + SIZE2)
|
||||
.add(SIZE2, SIZE * 2 + SIZE2);
|
||||
graphic.drawPolygon(pol, TESTSTYLE);
|
||||
graphic.drawPolygon(pol, Style.THIN);
|
||||
graphic.drawText(new Vector(SIZE2 + SIZE * 2, SIZE + SIZE2), new Vector(SIZE * 4, SIZE + SIZE2), "Test", Orientation.CENTERCENTER, Style.NORMAL);
|
||||
graphic.drawText(new Vector(SIZE2 + SIZE * 2, 0), new Vector(SIZE * 4, 0), label, Orientation.CENTERBOTTOM, Style.NORMAL);
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ import de.neemann.digital.core.element.Rotation;
|
||||
import de.neemann.digital.core.io.IntFormat;
|
||||
import de.neemann.digital.core.memory.DataField;
|
||||
import de.neemann.digital.core.memory.ROM;
|
||||
import de.neemann.digital.gui.components.test.TestData;
|
||||
import de.neemann.digital.gui.components.test.TestDataEditor;
|
||||
import de.neemann.digital.lang.Lang;
|
||||
import de.neemann.gui.ErrorMessage;
|
||||
import de.neemann.gui.ToolTipAction;
|
||||
@ -40,6 +42,7 @@ public final class EditorFactory {
|
||||
add(Rotation.class, RotationEditor.class);
|
||||
add(IntFormat.class, IntFormatsEditor.class);
|
||||
add(Language.class, LanguageEditor.class);
|
||||
add(TestData.class, TestDataEditor.class);
|
||||
}
|
||||
|
||||
private <T> void add(Class<T> clazz, Class<? extends Editor<T>> editor) {
|
||||
@ -67,7 +70,12 @@ public final class EditorFactory {
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class LabelEditor<T> implements Editor<T> {
|
||||
/**
|
||||
* Simple single component editor
|
||||
*
|
||||
* @param <T> the type to edit
|
||||
*/
|
||||
public static abstract class LabelEditor<T> implements Editor<T> {
|
||||
@Override
|
||||
public void addToPanel(JPanel panel, Key key, ElementAttributes elementAttributes) {
|
||||
JLabel label = new JLabel(key.getName() + ": ");
|
||||
@ -78,6 +86,12 @@ public final class EditorFactory {
|
||||
panel.add(component, DialogLayout.INPUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the editor component
|
||||
*
|
||||
* @param elementAttributes the elements attributes
|
||||
* @return the component
|
||||
*/
|
||||
protected abstract JComponent getComponent(ElementAttributes elementAttributes);
|
||||
}
|
||||
|
||||
@ -241,7 +255,7 @@ public final class EditorFactory {
|
||||
}
|
||||
}
|
||||
}
|
||||
.setActive(attr.getFile(ROM.LAST_DATA_FILE_KEY) != null)
|
||||
.setActive(attr.getFile(ROM.LAST_DATA_FILE_KEY) != null)
|
||||
.setToolTip(Lang.get("btn_reload_tt"))
|
||||
.createJButton()
|
||||
);
|
||||
|
@ -0,0 +1,24 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
/**
|
||||
* @author hneemann
|
||||
*/
|
||||
public class DataException extends Exception {
|
||||
/**
|
||||
* creates a new instance
|
||||
*
|
||||
* @param cause the cause
|
||||
*/
|
||||
public DataException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a new instance
|
||||
*
|
||||
* @param message the message
|
||||
*/
|
||||
public DataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import de.neemann.digital.core.Model;
|
||||
import de.neemann.digital.core.NodeException;
|
||||
import de.neemann.digital.core.ObservableValues;
|
||||
import de.neemann.digital.core.element.*;
|
||||
|
||||
/**
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestCaseElement implements Element {
|
||||
|
||||
/**
|
||||
* the used {@link ElementAttributes} key
|
||||
*/
|
||||
public static final Key<TestData> TESTDATA = new Key<>("Testdata", TestData.DEFAULT);
|
||||
|
||||
/**
|
||||
* The TestCaseElement description
|
||||
*/
|
||||
public static final ElementTypeDescription TESTCASEDESCRIPTION
|
||||
= new ElementTypeDescription("Testcase", TestCaseElement.class)
|
||||
.addAttribute(Keys.LABEL)
|
||||
.addAttribute(TESTDATA);
|
||||
|
||||
/**
|
||||
* creates a new instance
|
||||
*
|
||||
* @param attributes the attributes
|
||||
*/
|
||||
public TestCaseElement(ElementAttributes attributes) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInputs(ObservableValues inputs) throws NodeException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableValues getOutputs() {
|
||||
return ObservableValues.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerNodes(Model model) {
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* The test data
|
||||
*
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestData implements Iterable<int[]> {
|
||||
|
||||
/**
|
||||
* the default instance
|
||||
*/
|
||||
public static final TestData DEFAULT = new TestData("");
|
||||
|
||||
private String dataString;
|
||||
private transient ArrayList<int[]> lines;
|
||||
private transient ArrayList<String> names;
|
||||
|
||||
private TestData(String data) {
|
||||
this.dataString = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a new instance
|
||||
*
|
||||
* @param valueToCopy the instance to copy
|
||||
*/
|
||||
public TestData(TestData valueToCopy) {
|
||||
this(valueToCopy.dataString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<int[]> iterator() {
|
||||
return lines.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the data string
|
||||
*/
|
||||
public String getDataString() {
|
||||
return dataString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the data and checks its validity
|
||||
*
|
||||
* @param data the data
|
||||
* @throws DataException thrown if data is not valid
|
||||
*/
|
||||
public void setDataString(String data) throws DataException {
|
||||
if (!data.equals(dataString)) {
|
||||
TestDataParser tdp = new TestDataParser(data).parse();
|
||||
dataString = data;
|
||||
lines = tdp.getLines();
|
||||
names = tdp.getNames();
|
||||
}
|
||||
}
|
||||
|
||||
private void check() {
|
||||
if (lines == null) {
|
||||
try {
|
||||
TestDataParser tdp = new TestDataParser(dataString).parse();
|
||||
lines = tdp.getLines();
|
||||
names = tdp.getNames();
|
||||
} catch (DataException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the data lines
|
||||
*/
|
||||
public ArrayList<int[]> getLines() {
|
||||
check();
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the signal names
|
||||
*/
|
||||
public ArrayList<String> getNames() {
|
||||
check();
|
||||
return names;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import de.neemann.digital.lang.Lang;
|
||||
import de.neemann.gui.ErrorMessage;
|
||||
import de.neemann.gui.ToolTipAction;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
/**
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestDataDialog extends JDialog {
|
||||
|
||||
/**
|
||||
* Creates a new data dialog
|
||||
*
|
||||
* @param parent the parent component
|
||||
* @param data the data to edit
|
||||
*/
|
||||
public TestDataDialog(JComponent parent, TestData data) {
|
||||
super(SwingUtilities.getWindowAncestor(parent), Lang.get("key_Testdata"), ModalityType.APPLICATION_MODAL);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
|
||||
JTextArea text = new JTextArea(data.getDataString(), 30, 30);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(text);
|
||||
getContentPane().add(scrollPane);
|
||||
scrollPane.setRowHeaderView(new TextLineNumber(text, 3));
|
||||
|
||||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
buttons.add(new ToolTipAction(Lang.get("ok")) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
try {
|
||||
data.setDataString(text.getText());
|
||||
dispose();
|
||||
} catch (DataException e1) {
|
||||
new ErrorMessage(e1.getMessage()).show(TestDataDialog.this);
|
||||
}
|
||||
}
|
||||
}.createJButton());
|
||||
|
||||
getContentPane().add(buttons, BorderLayout.SOUTH);
|
||||
|
||||
pack();
|
||||
setLocationRelativeTo(parent);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import de.neemann.digital.core.element.ElementAttributes;
|
||||
import de.neemann.digital.core.element.Key;
|
||||
import de.neemann.digital.core.memory.DataField;
|
||||
import de.neemann.digital.gui.components.EditorFactory;
|
||||
import de.neemann.digital.lang.Lang;
|
||||
import de.neemann.gui.ToolTipAction;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
/**
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestDataEditor extends EditorFactory.LabelEditor<TestData> {
|
||||
|
||||
private final TestData data;
|
||||
private JButton editButton;
|
||||
|
||||
/**
|
||||
* Creates a new editor
|
||||
*
|
||||
* @param data the data to edit
|
||||
* @param key the data key
|
||||
*/
|
||||
public TestDataEditor(TestData data, Key<DataField> key) {
|
||||
this.data = new TestData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TestData getValue() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JComponent getComponent(ElementAttributes elementAttributes) {
|
||||
editButton = new ToolTipAction(Lang.get("btn_edit")) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
new TestDataDialog(editButton, data).setVisible(true);
|
||||
}
|
||||
}.createJButton();
|
||||
return editButton;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import de.neemann.digital.lang.Lang;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestDataParser {
|
||||
|
||||
private final BufferedReader r;
|
||||
private final ArrayList<int[]> lines;
|
||||
private final ArrayList<String> names;
|
||||
private int lineNumber;
|
||||
|
||||
/**
|
||||
* Create a new parser
|
||||
*
|
||||
* @param data the string to parse
|
||||
*/
|
||||
public TestDataParser(String data) {
|
||||
lines = new ArrayList<>();
|
||||
names = new ArrayList<>();
|
||||
r = new BufferedReader(new StringReader(data));
|
||||
lineNumber = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the string
|
||||
*
|
||||
* @return this for chained calls
|
||||
* @throws DataException DataException
|
||||
*/
|
||||
public TestDataParser parse() throws DataException {
|
||||
try {
|
||||
String header = readNonEmptyLine(r);
|
||||
if (header != null) {
|
||||
StringTokenizer tok = new StringTokenizer(header);
|
||||
while (tok.hasMoreElements())
|
||||
names.add(tok.nextToken());
|
||||
|
||||
String line;
|
||||
while ((line = readNonEmptyLine(r)) != null) {
|
||||
int[] row = new int[names.size()];
|
||||
tok = new StringTokenizer(line);
|
||||
int cols = tok.countTokens();
|
||||
if (cols != names.size())
|
||||
throw new DataException(Lang.get("err_testDataExpected_N0_found_N1_numbersInLine_N2", names.size(), cols, lineNumber));
|
||||
|
||||
for (int i = 0; i < cols; i++) {
|
||||
String num = null;
|
||||
try {
|
||||
num = tok.nextToken();
|
||||
if (num.toUpperCase().equals("X"))
|
||||
row[i] = -1;
|
||||
else
|
||||
row[i] = Integer.parseInt(num);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new DataException(Lang.get("err_notANumber_N0_inLine_N1", num, lineNumber));
|
||||
}
|
||||
}
|
||||
lines.add(row);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new DataException(e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private String readNonEmptyLine(BufferedReader r) throws IOException {
|
||||
while (true) {
|
||||
lineNumber++;
|
||||
String line = r.readLine();
|
||||
if (line == null || (line.length() > 0 && line.charAt(0) != '#'))
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the data lines
|
||||
*/
|
||||
public ArrayList<int[]> getLines() {
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the signal names
|
||||
*/
|
||||
public ArrayList<String> getNames() {
|
||||
return names;
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import javax.swing.event.TableModelListener;
|
||||
import javax.swing.table.TableModel;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestResult implements TableModel {
|
||||
|
||||
private final ArrayList<String> names;
|
||||
private final ArrayList<Line> lines;
|
||||
|
||||
/**
|
||||
* Creates a new test result
|
||||
*
|
||||
* @param testData the test data
|
||||
*/
|
||||
public TestResult(TestData testData) {
|
||||
names = testData.getNames();
|
||||
lines = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return lines.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return names.size() + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int columnIndex) {
|
||||
if (columnIndex < names.size())
|
||||
return names.get(columnIndex);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex) {
|
||||
if (columnIndex < names.size())
|
||||
return Integer.class;
|
||||
else
|
||||
return Boolean.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
return lines.get(rowIndex).getCol(columnIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTableModelListener(TableModelListener l) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeTableModelListener(TableModelListener l) {
|
||||
}
|
||||
|
||||
private class Line {
|
||||
private final int[] data;
|
||||
private final boolean passed;
|
||||
|
||||
Line(int[] data, boolean passed) {
|
||||
this.data = data;
|
||||
this.passed = passed;
|
||||
}
|
||||
|
||||
Object getCol(int columnIndex) {
|
||||
if (columnIndex < names.size())
|
||||
return data[columnIndex];
|
||||
else
|
||||
return passed;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,430 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.border.CompoundBorder;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.MatteBorder;
|
||||
import javax.swing.event.CaretEvent;
|
||||
import javax.swing.event.CaretListener;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.text.*;
|
||||
import java.awt.*;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* This class will display line numbers for a related text component. The text
|
||||
* component must use the same line height for each line. TextLineNumber
|
||||
* supports wrapped lines and will highlight the line number of the current
|
||||
* line in the text component.
|
||||
*
|
||||
* This class was designed to be used as a component added to the row header
|
||||
* of a JScrollPane.
|
||||
*
|
||||
* Written by Rob Camick, May 23, 2009
|
||||
* See: https://tips4java.wordpress.com/2009/05/23/text-component-line-number/
|
||||
*/
|
||||
public class TextLineNumber extends JPanel
|
||||
implements CaretListener, DocumentListener, PropertyChangeListener {
|
||||
public final static float LEFT = 0.0f;
|
||||
public final static float CENTER = 0.5f;
|
||||
public final static float RIGHT = 1.0f;
|
||||
|
||||
private final static Border OUTER = new MatteBorder(0, 0, 0, 2, Color.GRAY);
|
||||
|
||||
private final static int HEIGHT = Integer.MAX_VALUE - 1000000;
|
||||
|
||||
// Text component this TextTextLineNumber component is in sync with
|
||||
|
||||
private JTextComponent component;
|
||||
|
||||
// Properties that can be changed
|
||||
|
||||
private boolean updateFont;
|
||||
private int borderGap;
|
||||
private Color currentLineForeground;
|
||||
private float digitAlignment;
|
||||
private int minimumDisplayDigits;
|
||||
|
||||
// Keep history information to reduce the number of times the component
|
||||
// needs to be repainted
|
||||
|
||||
private int lastDigits;
|
||||
private int lastHeight;
|
||||
private int lastLine;
|
||||
|
||||
private HashMap<String, FontMetrics> fonts;
|
||||
|
||||
/**
|
||||
* Create a line number component for a text component. This minimum
|
||||
* display width will be based on 3 digits.
|
||||
*
|
||||
* @param component the related text component
|
||||
*/
|
||||
public TextLineNumber(JTextComponent component) {
|
||||
this(component, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a line number component for a text component.
|
||||
*
|
||||
* @param component the related text component
|
||||
* @param minimumDisplayDigits the number of digits used to calculate
|
||||
* the minimum width of the component
|
||||
*/
|
||||
public TextLineNumber(JTextComponent component, int minimumDisplayDigits) {
|
||||
this.component = component;
|
||||
|
||||
setFont(component.getFont());
|
||||
|
||||
setBorderGap(5);
|
||||
setCurrentLineForeground(Color.RED);
|
||||
setDigitAlignment(RIGHT);
|
||||
setMinimumDisplayDigits(minimumDisplayDigits);
|
||||
|
||||
component.getDocument().addDocumentListener(this);
|
||||
component.addCaretListener(this);
|
||||
component.addPropertyChangeListener("font", this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the update font property
|
||||
*
|
||||
* @return the update font property
|
||||
*/
|
||||
public boolean getUpdateFont() {
|
||||
return updateFont;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the update font property. Indicates whether this Font should be
|
||||
* updated automatically when the Font of the related text component
|
||||
* is changed.
|
||||
*
|
||||
* @param updateFont when true update the Font and repaint the line
|
||||
* numbers, otherwise just repaint the line numbers.
|
||||
*/
|
||||
public void setUpdateFont(boolean updateFont) {
|
||||
this.updateFont = updateFont;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the border gap
|
||||
*
|
||||
* @return the border gap in pixels
|
||||
*/
|
||||
public int getBorderGap() {
|
||||
return borderGap;
|
||||
}
|
||||
|
||||
/**
|
||||
* The border gap is used in calculating the left and right insets of the
|
||||
* border. Default value is 5.
|
||||
*
|
||||
* @param borderGap the gap in pixels
|
||||
*/
|
||||
public void setBorderGap(int borderGap) {
|
||||
this.borderGap = borderGap;
|
||||
Border inner = new EmptyBorder(0, borderGap, 0, borderGap);
|
||||
setBorder(new CompoundBorder(OUTER, inner));
|
||||
lastDigits = 0;
|
||||
setPreferredWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current line rendering Color
|
||||
*
|
||||
* @return the Color used to render the current line number
|
||||
*/
|
||||
public Color getCurrentLineForeground() {
|
||||
return currentLineForeground == null ? getForeground() : currentLineForeground;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Color used to render the current line digits. Default is Coolor.RED.
|
||||
*
|
||||
* @param currentLineForeground the Color used to render the current line
|
||||
*/
|
||||
public void setCurrentLineForeground(Color currentLineForeground) {
|
||||
this.currentLineForeground = currentLineForeground;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the digit alignment
|
||||
*
|
||||
* @return the alignment of the painted digits
|
||||
*/
|
||||
public float getDigitAlignment() {
|
||||
return digitAlignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the horizontal alignment of the digits within the component.
|
||||
* Common values would be:
|
||||
* <ul>
|
||||
* <li>TextLineNumber.LEFT
|
||||
* <li>TextLineNumber.CENTER
|
||||
* <li>TextLineNumber.RIGHT (default)
|
||||
* </ul>
|
||||
*/
|
||||
public void setDigitAlignment(float digitAlignment) {
|
||||
this.digitAlignment =
|
||||
digitAlignment > 1.0f ? 1.0f : digitAlignment < 0.0f ? -1.0f : digitAlignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum display digits
|
||||
*
|
||||
* @return the minimum display digits
|
||||
*/
|
||||
public int getMinimumDisplayDigits() {
|
||||
return minimumDisplayDigits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the mimimum number of digits used to calculate the preferred
|
||||
* width of the component. Default is 3.
|
||||
*
|
||||
* @param minimumDisplayDigits the number digits used in the preferred
|
||||
* width calculation
|
||||
*/
|
||||
public void setMinimumDisplayDigits(int minimumDisplayDigits) {
|
||||
this.minimumDisplayDigits = minimumDisplayDigits;
|
||||
setPreferredWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the width needed to display the maximum line number
|
||||
*/
|
||||
private void setPreferredWidth() {
|
||||
Element root = component.getDocument().getDefaultRootElement();
|
||||
int lines = root.getElementCount();
|
||||
int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits);
|
||||
|
||||
// Update sizes when number of digits in the line number changes
|
||||
|
||||
if (lastDigits != digits) {
|
||||
lastDigits = digits;
|
||||
FontMetrics fontMetrics = getFontMetrics(getFont());
|
||||
int width = fontMetrics.charWidth('0') * digits;
|
||||
Insets insets = getInsets();
|
||||
int preferredWidth = insets.left + insets.right + width;
|
||||
|
||||
Dimension d = getPreferredSize();
|
||||
d.setSize(preferredWidth, HEIGHT);
|
||||
setPreferredSize(d);
|
||||
setSize(d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the line numbers
|
||||
*/
|
||||
@Override
|
||||
public void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
|
||||
// Determine the width of the space available to draw the line number
|
||||
|
||||
FontMetrics fontMetrics = component.getFontMetrics(component.getFont());
|
||||
Insets insets = getInsets();
|
||||
int availableWidth = getSize().width - insets.left - insets.right;
|
||||
|
||||
// Determine the rows to draw within the clipped bounds.
|
||||
|
||||
Rectangle clip = g.getClipBounds();
|
||||
int rowStartOffset = component.viewToModel(new Point(0, clip.y));
|
||||
int endOffset = component.viewToModel(new Point(0, clip.y + clip.height));
|
||||
|
||||
while (rowStartOffset <= endOffset) {
|
||||
try {
|
||||
if (isCurrentLine(rowStartOffset))
|
||||
g.setColor(getCurrentLineForeground());
|
||||
else
|
||||
g.setColor(getForeground());
|
||||
|
||||
// Get the line number as a string and then determine the
|
||||
// "X" and "Y" offsets for drawing the string.
|
||||
|
||||
String lineNumber = getTextLineNumber(rowStartOffset);
|
||||
int stringWidth = fontMetrics.stringWidth(lineNumber);
|
||||
int x = getOffsetX(availableWidth, stringWidth) + insets.left;
|
||||
int y = getOffsetY(rowStartOffset, fontMetrics);
|
||||
g.drawString(lineNumber, x, y);
|
||||
|
||||
// Move to the next row
|
||||
|
||||
rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1;
|
||||
} catch (Exception e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* We need to know if the caret is currently positioned on the line we
|
||||
* are about to paint so the line number can be highlighted.
|
||||
*/
|
||||
private boolean isCurrentLine(int rowStartOffset) {
|
||||
int caretPosition = component.getCaretPosition();
|
||||
Element root = component.getDocument().getDefaultRootElement();
|
||||
|
||||
if (root.getElementIndex(rowStartOffset) == root.getElementIndex(caretPosition))
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the line number to be drawn. The empty string will be returned
|
||||
* when a line of text has wrapped.
|
||||
*/
|
||||
protected String getTextLineNumber(int rowStartOffset) {
|
||||
Element root = component.getDocument().getDefaultRootElement();
|
||||
int index = root.getElementIndex(rowStartOffset);
|
||||
Element line = root.getElement(index);
|
||||
|
||||
if (line.getStartOffset() == rowStartOffset)
|
||||
return String.valueOf(index + 1);
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine the X offset to properly align the line number when drawn
|
||||
*/
|
||||
private int getOffsetX(int availableWidth, int stringWidth) {
|
||||
return (int) ((availableWidth - stringWidth) * digitAlignment);
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine the Y offset for the current row
|
||||
*/
|
||||
private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics)
|
||||
throws BadLocationException {
|
||||
// Get the bounding rectangle of the row
|
||||
|
||||
Rectangle r = component.modelToView(rowStartOffset);
|
||||
int lineHeight = fontMetrics.getHeight();
|
||||
int y = r.y + r.height;
|
||||
int descent = 0;
|
||||
|
||||
// The text needs to be positioned above the bottom of the bounding
|
||||
// rectangle based on the descent of the font(s) contained on the row.
|
||||
|
||||
if (r.height == lineHeight) // default font is being used
|
||||
{
|
||||
descent = fontMetrics.getDescent();
|
||||
} else // We need to check all the attributes for font changes
|
||||
{
|
||||
if (fonts == null)
|
||||
fonts = new HashMap<String, FontMetrics>();
|
||||
|
||||
Element root = component.getDocument().getDefaultRootElement();
|
||||
int index = root.getElementIndex(rowStartOffset);
|
||||
Element line = root.getElement(index);
|
||||
|
||||
for (int i = 0; i < line.getElementCount(); i++) {
|
||||
Element child = line.getElement(i);
|
||||
AttributeSet as = child.getAttributes();
|
||||
String fontFamily = (String) as.getAttribute(StyleConstants.FontFamily);
|
||||
Integer fontSize = (Integer) as.getAttribute(StyleConstants.FontSize);
|
||||
String key = fontFamily + fontSize;
|
||||
|
||||
FontMetrics fm = fonts.get(key);
|
||||
|
||||
if (fm == null) {
|
||||
Font font = new Font(fontFamily, Font.PLAIN, fontSize);
|
||||
fm = component.getFontMetrics(font);
|
||||
fonts.put(key, fm);
|
||||
}
|
||||
|
||||
descent = Math.max(descent, fm.getDescent());
|
||||
}
|
||||
}
|
||||
|
||||
return y - descent;
|
||||
}
|
||||
|
||||
//
|
||||
// Implement CaretListener interface
|
||||
//
|
||||
@Override
|
||||
public void caretUpdate(CaretEvent e) {
|
||||
// Get the line the caret is positioned on
|
||||
|
||||
int caretPosition = component.getCaretPosition();
|
||||
Element root = component.getDocument().getDefaultRootElement();
|
||||
int currentLine = root.getElementIndex(caretPosition);
|
||||
|
||||
// Need to repaint so the correct line number can be highlighted
|
||||
|
||||
if (lastLine != currentLine) {
|
||||
repaint();
|
||||
lastLine = currentLine;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Implement DocumentListener interface
|
||||
//
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
documentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
documentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
documentChanged();
|
||||
}
|
||||
|
||||
/*
|
||||
* A document change may affect the number of displayed lines of text.
|
||||
* Therefore the lines numbers will also change.
|
||||
*/
|
||||
private void documentChanged() {
|
||||
// View of the component has not been updated at the time
|
||||
// the DocumentEvent is fired
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
int endPos = component.getDocument().getLength();
|
||||
Rectangle rect = component.modelToView(endPos);
|
||||
|
||||
if (rect != null && rect.y != lastHeight) {
|
||||
setPreferredWidth();
|
||||
repaint();
|
||||
lastHeight = rect.y;
|
||||
}
|
||||
} catch (BadLocationException ex) { /* nothing to do */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Implement PropertyChangeListener interface
|
||||
//
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (evt.getNewValue() instanceof Font) {
|
||||
if (updateFont) {
|
||||
Font newFont = (Font) evt.getNewValue();
|
||||
setFont(newFont);
|
||||
lastDigits = 0;
|
||||
setPreferredWidth();
|
||||
} else {
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Classes to handle test cases
|
||||
*
|
||||
* @author hneemann
|
||||
*/
|
||||
package de.neemann.digital.gui.components.test;
|
@ -104,6 +104,8 @@ in der Stabilisierungsphase befindet. Hat sich die Schaltung stabilisiert wird d
|
||||
<string name="elem_Text_tt">Zeigt einen einfachen Text in der Schaltung an.</string>
|
||||
<string name="elem_XNOr">Nicht Exclusiv Oder</string>
|
||||
<string name="elem_XOr">Exclusiv Oder</string>
|
||||
<string name="elem_Testcase">Testfall</string>
|
||||
<string name="elem_Testcase_tt">Beschreibt einen Testfall</string>
|
||||
<string name="error">Fehler</string>
|
||||
<string name="err_DFlipflopWithoutALabel">Flipflop hat keine Bezeichnung!</string>
|
||||
<string name="err_N_isNotInputOrOutput">Pin {0} in Element {1} ist werder Eingang noch Ausgang</string>
|
||||
@ -164,6 +166,8 @@ Zur Analyse können Sie die Schaltung im Gatterschrittmodus ausführen.</string>
|
||||
<string name="err_parserUnexpectedToken_N">Unerwartetes Zeichen {0}</string>
|
||||
<string name="err_parserMissingClosedParenthesis">Fehlende schließende Klammer</string>
|
||||
<string name="err_parserUnexpectedEndOfExpression">Unerwartetes Ende des Ausdrucks</string>
|
||||
<string name="err_notANumber_N0_inLine_N1">Wert {0} in Zeile {1} ist keine Zahl!</string>
|
||||
<string name="err_testDataExpected_N0_found_N1_numbersInLine_N2">Erwarte {0} anstelle von {1} Werten in Zeile {2}!</string>
|
||||
<string name="key_AddrBits">Adress Bits</string>
|
||||
<string name="key_Bits">Datenbits</string>
|
||||
<string name="key_Color">Farbe</string>
|
||||
@ -214,6 +218,7 @@ Zur Analyse können Sie die Schaltung im Gatterschrittmodus ausführen.</string>
|
||||
<string name="key_termWidth">Zeichen pro Zeile</string>
|
||||
<string name="key_valueIsProbe">Als Messwert verwenden</string>
|
||||
<string name="key_valueIsProbe_tt">Wenn gesetzt, taucht der Wert als Messwert in Graph und Tabelle auf</string>
|
||||
<string name="key_Testdata">Testdaten</string>
|
||||
<string name="lib_Logic">Logisch</string>
|
||||
<string name="lib_arithmetic">Arithmetik</string>
|
||||
<string name="lib_flipFlops">FlipFlops</string>
|
||||
@ -221,6 +226,7 @@ Zur Analyse können Sie die Schaltung im Gatterschrittmodus ausführen.</string>
|
||||
<string name="lib_memory">Speicher</string>
|
||||
<string name="lib_mux">Multiplexer</string>
|
||||
<string name="lib_wires">Leitungen</string>
|
||||
<string name="lib_test">Test</string>
|
||||
<string name="menu_about">Über Digital</string>
|
||||
<string name="menu_analyse">Analyse</string>
|
||||
<string name="menu_analyse_tt">Analyse der aktuellen Schaltung</string>
|
||||
|
@ -104,6 +104,8 @@ The terminal opens its own window.</string>
|
||||
<string name="elem_Text_tt">Shows a text in the circuit</string>
|
||||
<string name="elem_XNOr">XNOr</string>
|
||||
<string name="elem_XOr">XOr</string>
|
||||
<string name="elem_Testcase">Test case</string>
|
||||
<string name="elem_Testcase_tt">Describes a single test case.</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="err_DFlipflopWithoutALabel">D-Flipflop has no label set</string>
|
||||
<string name="err_N_isNotInputOrOutput">Pin {0} in element {1} is not a input or output</string>
|
||||
@ -164,6 +166,8 @@ To analyse you can run the circuit in single gate step mode.</string>
|
||||
<string name="err_parserUnexpectedToken_N">Unexpected Token {0}</string>
|
||||
<string name="err_parserMissingClosedParenthesis">Missing closed parenthesis</string>
|
||||
<string name="err_parserUnexpectedEndOfExpression">Unexpected end of expression</string>
|
||||
<string name="err_notANumber_N0_inLine_N1">Value {0} in line {1} is not a number!</string>
|
||||
<string name="err_testDataExpected_N0_found_N1_numbersInLine_N2">Expected {0} but found {1} values in line {2}!</string>
|
||||
<string name="key_AddrBits">Address Bits</string>
|
||||
<string name="key_Bits">Data Bits</string>
|
||||
<string name="key_Color">Color</string>
|
||||
@ -214,6 +218,7 @@ To analyse you can run the circuit in single gate step mode.</string>
|
||||
<string name="key_termWidth">Characters per line</string>
|
||||
<string name="key_valueIsProbe">Use as measurment value</string>
|
||||
<string name="key_valueIsProbe_tt">Is set the value is a measurement value and appears in the graph and data table.</string>
|
||||
<string name="key_Testdata">Testdata</string>
|
||||
<string name="lib_Logic">Logic</string>
|
||||
<string name="lib_arithmetic">Arithmetic</string>
|
||||
<string name="lib_flipFlops">FlipFlops</string>
|
||||
@ -221,6 +226,7 @@ To analyse you can run the circuit in single gate step mode.</string>
|
||||
<string name="lib_memory">Memory</string>
|
||||
<string name="lib_mux">Plexers</string>
|
||||
<string name="lib_wires">Wires</string>
|
||||
<string name="lib_test">Test</string>
|
||||
<string name="menu_about">About</string>
|
||||
<string name="menu_analyse">Analyse</string>
|
||||
<string name="menu_analyse_tt">Analyses the actual circuit</string>
|
||||
|
@ -0,0 +1,41 @@
|
||||
package de.neemann.digital.gui.components.test;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* @author hneemann
|
||||
*/
|
||||
public class TestDataParserTest extends TestCase {
|
||||
|
||||
public void testOk() throws DataException {
|
||||
TestDataParser td = new TestDataParser("A B\n0 1\n1 0\nX x").parse();
|
||||
assertEquals(2,td.getNames().size());
|
||||
assertEquals(3,td.getLines().size());
|
||||
|
||||
assertEquals(0, td.getLines().get(0)[0]);
|
||||
assertEquals(1, td.getLines().get(0)[1]);
|
||||
assertEquals(1, td.getLines().get(1)[0]);
|
||||
assertEquals(0, td.getLines().get(1)[1]);
|
||||
assertEquals(-1, td.getLines().get(2)[0]);
|
||||
assertEquals(-1, td.getLines().get(2)[1]);
|
||||
}
|
||||
|
||||
public void testMissingValue() {
|
||||
try {
|
||||
new TestDataParser("A B\n0 0\n1").parse();
|
||||
assertTrue(false);
|
||||
} catch (DataException e) {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void testInvalidValue() {
|
||||
try {
|
||||
new TestDataParser("A B\n0 0\n1 u").parse();
|
||||
assertTrue(false);
|
||||
} catch (DataException e) {
|
||||
assertTrue(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user