diff --git a/src/main/java/de/neemann/digital/core/element/Key.java b/src/main/java/de/neemann/digital/core/element/Key.java index db7cb92bf..a23ae4e54 100644 --- a/src/main/java/de/neemann/digital/core/element/Key.java +++ b/src/main/java/de/neemann/digital/core/element/Key.java @@ -14,7 +14,13 @@ public class Key { 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); diff --git a/src/main/java/de/neemann/digital/draw/elements/Circuit.java b/src/main/java/de/neemann/digital/draw/elements/Circuit.java index 8ea4f7b00..c2cdf04ba 100644 --- a/src/main/java/de/neemann/digital/draw/elements/Circuit.java +++ b/src/main/java/de/neemann/digital/draw/elements/Circuit.java @@ -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; } diff --git a/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java b/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java index fcb0d2bb7..28b6efae1 100644 --- a/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java +++ b/src/main/java/de/neemann/digital/draw/library/ElementLibrary.java @@ -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 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) { diff --git a/src/main/java/de/neemann/digital/draw/shapes/ShapeFactory.java b/src/main/java/de/neemann/digital/draw/shapes/ShapeFactory.java index b62a554c3..f681cde83 100644 --- a/src/main/java/de/neemann/digital/draw/shapes/ShapeFactory.java +++ b/src/main/java/de/neemann/digital/draw/shapes/ShapeFactory.java @@ -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); } } diff --git a/src/main/java/de/neemann/digital/draw/shapes/TestCaseShape.java b/src/main/java/de/neemann/digital/draw/shapes/TestCaseShape.java new file mode 100644 index 000000000..4da437587 --- /dev/null +++ b/src/main/java/de/neemann/digital/draw/shapes/TestCaseShape.java @@ -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); + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/EditorFactory.java b/src/main/java/de/neemann/digital/gui/components/EditorFactory.java index 79a91aba0..919bd3f8c 100644 --- a/src/main/java/de/neemann/digital/gui/components/EditorFactory.java +++ b/src/main/java/de/neemann/digital/gui/components/EditorFactory.java @@ -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 void add(Class clazz, Class> editor) { @@ -67,7 +70,12 @@ public final class EditorFactory { } } - private static abstract class LabelEditor implements Editor { + /** + * Simple single component editor + * + * @param the type to edit + */ + public static abstract class LabelEditor implements Editor { @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() ); diff --git a/src/main/java/de/neemann/digital/gui/components/test/DataException.java b/src/main/java/de/neemann/digital/gui/components/test/DataException.java new file mode 100644 index 000000000..adcba1ded --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/DataException.java @@ -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); + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/TestCaseElement.java b/src/main/java/de/neemann/digital/gui/components/test/TestCaseElement.java new file mode 100644 index 000000000..18bb10929 --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/TestCaseElement.java @@ -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 = 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) { + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/TestData.java b/src/main/java/de/neemann/digital/gui/components/test/TestData.java new file mode 100644 index 000000000..24ea63fd3 --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/TestData.java @@ -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 { + + /** + * the default instance + */ + public static final TestData DEFAULT = new TestData(""); + + private String dataString; + private transient ArrayList lines; + private transient ArrayList 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 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 getLines() { + check(); + return lines; + } + + /** + * @return the signal names + */ + public ArrayList getNames() { + check(); + return names; + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/TestDataDialog.java b/src/main/java/de/neemann/digital/gui/components/test/TestDataDialog.java new file mode 100644 index 000000000..57cd18f8a --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/TestDataDialog.java @@ -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); + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/TestDataEditor.java b/src/main/java/de/neemann/digital/gui/components/test/TestDataEditor.java new file mode 100644 index 000000000..981909324 --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/TestDataEditor.java @@ -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 { + + 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 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; + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/TestDataParser.java b/src/main/java/de/neemann/digital/gui/components/test/TestDataParser.java new file mode 100644 index 000000000..c1515966c --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/TestDataParser.java @@ -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 lines; + private final ArrayList 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 getLines() { + return lines; + } + + /** + * @return the signal names + */ + public ArrayList getNames() { + return names; + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/TestResult.java b/src/main/java/de/neemann/digital/gui/components/test/TestResult.java new file mode 100644 index 000000000..5da2f657f --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/TestResult.java @@ -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 names; + private final ArrayList 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; + } + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/TextLineNumber.java b/src/main/java/de/neemann/digital/gui/components/test/TextLineNumber.java new file mode 100644 index 000000000..5a7464a2d --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/TextLineNumber.java @@ -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 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: + *
    + *
  • TextLineNumber.LEFT + *
  • TextLineNumber.CENTER + *
  • TextLineNumber.RIGHT (default) + *
+ */ + 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(); + + 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(); + } + } + } +} diff --git a/src/main/java/de/neemann/digital/gui/components/test/package-info.java b/src/main/java/de/neemann/digital/gui/components/test/package-info.java new file mode 100644 index 000000000..735c93aa7 --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/components/test/package-info.java @@ -0,0 +1,6 @@ +/** + * Classes to handle test cases + * + * @author hneemann + */ +package de.neemann.digital.gui.components.test; diff --git a/src/main/resources/lang/lang_de.xml b/src/main/resources/lang/lang_de.xml index 792e4f853..e92c08998 100644 --- a/src/main/resources/lang/lang_de.xml +++ b/src/main/resources/lang/lang_de.xml @@ -104,6 +104,8 @@ in der Stabilisierungsphase befindet. Hat sich die Schaltung stabilisiert wird d Zeigt einen einfachen Text in der Schaltung an. Nicht Exclusiv Oder Exclusiv Oder + Testfall + Beschreibt einen Testfall Fehler Flipflop hat keine Bezeichnung! Pin {0} in Element {1} ist werder Eingang noch Ausgang @@ -164,6 +166,8 @@ Zur Analyse können Sie die Schaltung im Gatterschrittmodus ausführen.
Unerwartetes Zeichen {0} Fehlende schließende Klammer Unerwartetes Ende des Ausdrucks + Wert {0} in Zeile {1} ist keine Zahl! + Erwarte {0} anstelle von {1} Werten in Zeile {2}! Adress Bits Datenbits Farbe @@ -214,6 +218,7 @@ Zur Analyse können Sie die Schaltung im Gatterschrittmodus ausführen.
Zeichen pro Zeile Als Messwert verwenden Wenn gesetzt, taucht der Wert als Messwert in Graph und Tabelle auf + Testdaten Logisch Arithmetik FlipFlops @@ -221,6 +226,7 @@ Zur Analyse können Sie die Schaltung im Gatterschrittmodus ausführen.
Speicher Multiplexer Leitungen + Test Über Digital Analyse Analyse der aktuellen Schaltung diff --git a/src/main/resources/lang/lang_en.xml b/src/main/resources/lang/lang_en.xml index c5d85c685..eaeaae7fc 100644 --- a/src/main/resources/lang/lang_en.xml +++ b/src/main/resources/lang/lang_en.xml @@ -104,6 +104,8 @@ The terminal opens its own window.
Shows a text in the circuit XNOr XOr + Test case + Describes a single test case. Error D-Flipflop has no label set Pin {0} in element {1} is not a input or output @@ -164,6 +166,8 @@ To analyse you can run the circuit in single gate step mode.
Unexpected Token {0} Missing closed parenthesis Unexpected end of expression + Value {0} in line {1} is not a number! + Expected {0} but found {1} values in line {2}! Address Bits Data Bits Color @@ -214,6 +218,7 @@ To analyse you can run the circuit in single gate step mode. Characters per line Use as measurment value Is set the value is a measurement value and appears in the graph and data table. + Testdata Logic Arithmetic FlipFlops @@ -221,6 +226,7 @@ To analyse you can run the circuit in single gate step mode. Memory Plexers Wires + Test About Analyse Analyses the actual circuit diff --git a/src/test/java/de/neemann/digital/gui/components/test/TestDataParserTest.java b/src/test/java/de/neemann/digital/gui/components/test/TestDataParserTest.java new file mode 100644 index 000000000..d4bcb7eaf --- /dev/null +++ b/src/test/java/de/neemann/digital/gui/components/test/TestDataParserTest.java @@ -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); + } + } + +} \ No newline at end of file