diff --git a/distribution/ReleaseNotes.txt b/distribution/ReleaseNotes.txt index aa977da50..ac72f5ca4 100644 --- a/distribution/ReleaseNotes.txt +++ b/distribution/ReleaseNotes.txt @@ -9,6 +9,7 @@ HEAD, planned as v0.26 - If a high-z value is connected to a logic gate input, the read value is undefined. - It is now possible to use a probe as output in a test case. +- Adds undo to text fields - Fixed a bug in the Demuxer Verilog template that causes problems when using multiple demuxers in the same circuit. - Generic circuits are easier to debug: It is possible now to create diff --git a/src/main/dig/hdl/HowTo.md b/src/main/dig/hdl/HowTo.md index 279edd501..2d5b8c7f8 100644 --- a/src/main/dig/hdl/HowTo.md +++ b/src/main/dig/hdl/HowTo.md @@ -4,7 +4,7 @@ ### The Commands to Execute -The `.config` File describes two things. At first it defines which external +The `.config` File describes two things. At first, it defines which external executables are needed to be started to run a circuit on a specific board. And second it creates the files which are required to run a circuit on a specific board. @@ -84,7 +84,7 @@ The `filter` attribute enables the filtering of the files content. And finally t is used to include this file file from other `.config` files. The `content` tag defines the content of the file created. In most cases the content of the file depends on the circuit. In this case a the `filter` -attribute needs to be set to `true`. After thät you are able to use the `` or `{?...?}` +attribute needs to be set to `true`. After that you are able to use the `` or `{?...?}` fragments to generate the file in a flexible way. As an example we want to create a file named `Pins_[name of circuit].pin` which contains all the pins used by the circuit: @@ -105,7 +105,7 @@ The circuit contains the following pins: ``` The code enclosed by the `{?...?}` characters is executed and prints the list of all -used pins to the text file. The `` statemant ensures that the content +used pins to the text file. The `` statement ensures that the content is considered as character data which is not analysed by the XML parser. If this statement is not used the `i < sizeOf(...` code causes an XML parser error. @@ -200,7 +200,7 @@ bool to it. ### File Creation -A template allways begins with plain text which is copied to the file to generate. +A template always begins with plain text which is copied to the file to generate. Special actions - data evaluations or control structures - are delimited by `{?` and `?}` or ``. Because the `.config` files are XML files the `{?` and `?}` variant is easier to use in most cases. diff --git a/src/main/java/de/neemann/digital/analyse/SubstituteLibrary.java b/src/main/java/de/neemann/digital/analyse/SubstituteLibrary.java index c85667a09..837d13c4a 100644 --- a/src/main/java/de/neemann/digital/analyse/SubstituteLibrary.java +++ b/src/main/java/de/neemann/digital/analyse/SubstituteLibrary.java @@ -17,7 +17,6 @@ import de.neemann.digital.draw.library.ElementNotFoundException; import de.neemann.digital.draw.library.LibraryInterface; import de.neemann.digital.hdl.hgs.*; import de.neemann.digital.lang.Lang; -import de.neemann.digital.testing.TestCaseDescription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -220,9 +219,6 @@ public class SubstituteLibrary implements LibraryInterface { } private Object doImplicitTypeCasts(Class expectedClass, Object val) { - if (expectedClass == TestCaseDescription.class) - return new TestCaseDescription(val.toString()); - if (expectedClass == Integer.class && val instanceof Long) { long l = (Long) val; if (l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE) diff --git a/src/main/java/de/neemann/digital/core/element/Keys.java b/src/main/java/de/neemann/digital/core/element/Keys.java index 9cceabaf3..231681773 100644 --- a/src/main/java/de/neemann/digital/core/element/Keys.java +++ b/src/main/java/de/neemann/digital/core/element/Keys.java @@ -874,6 +874,6 @@ public final class Keys { * The test data */ public static final Key TESTDATA = - new Key<>("Testdata", () -> new TestCaseDescription("")); + new Key<>("Testdata", TestCaseDescription::new); } diff --git a/src/main/java/de/neemann/digital/gui/components/AttributeDialog.java b/src/main/java/de/neemann/digital/gui/components/AttributeDialog.java index 131eb3da7..80a9eb7e6 100644 --- a/src/main/java/de/neemann/digital/gui/components/AttributeDialog.java +++ b/src/main/java/de/neemann/digital/gui/components/AttributeDialog.java @@ -177,7 +177,7 @@ public class AttributeDialog extends JDialog { final AbstractAction cancel = new AbstractAction(Lang.get("cancel")) { @Override public void actionPerformed(ActionEvent e) { - dispose(); + tryDispose(); } }; @@ -190,24 +190,7 @@ public class AttributeDialog extends JDialog { addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { - EditorHolder majorModification = null; - for (EditorHolder eh : editors) - if (eh.e.invisibleModification()) - majorModification = eh; - if (majorModification == null) - dispose(); - else { - int r = JOptionPane.showOptionDialog( - AttributeDialog.this, - Lang.get("msg_dataWillBeLost_n", majorModification.key.getName()), - Lang.get("msg_warning"), - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE, - null, new String[]{Lang.get("btn_discard"), Lang.get("cancel")}, - Lang.get("cancel")); - if (r == JOptionPane.YES_OPTION) - dispose(); - } + tryDispose(); } }); @@ -217,6 +200,27 @@ public class AttributeDialog extends JDialog { JComponent.WHEN_IN_FOCUSED_WINDOW); } + private void tryDispose() { + EditorHolder majorModification = null; + for (EditorHolder eh : editors) + if (eh.e.invisibleModification()) + majorModification = eh; + if (majorModification == null) + dispose(); + else { + int r = JOptionPane.showOptionDialog( + AttributeDialog.this, + Lang.get("msg_dataWillBeLost_n", majorModification.key.getName()), + Lang.get("msg_warning"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE, + null, new String[]{Lang.get("btn_discard"), Lang.get("btn_editFurther")}, + Lang.get("cancel")); + if (r == JOptionPane.YES_OPTION) + dispose(); + } + } + private EditorPanel findPanel(ArrayList panels, String panelId) { for (EditorPanel p : panels) if (panelId.equals(p.getPanelId())) 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 993f20890..8424dcd03 100644 --- a/src/main/java/de/neemann/digital/gui/components/EditorFactory.java +++ b/src/main/java/de/neemann/digital/gui/components/EditorFactory.java @@ -35,11 +35,9 @@ import de.neemann.gui.language.Language; import javax.swing.*; import javax.swing.text.JTextComponent; +import javax.swing.undo.UndoManager; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; +import java.awt.event.*; import java.io.*; import java.lang.reflect.Constructor; import java.util.ArrayList; @@ -177,6 +175,7 @@ public final class EditorFactory { private final JTextComponent text; private final JComponent compToAdd; + private final UndoManager undoManager; private JPopupMenu popup; public StringEditor(String value, Key key) { @@ -219,6 +218,8 @@ public final class EditorFactory { compToAdd = text; } text.setText(value); + + undoManager = createUndoManager(text); } JPopupMenu getPopupMenu(String keyName) { @@ -286,8 +287,10 @@ public final class EditorFactory { @Override public void setValue(String value) { - if (!text.getText().equals(value)) + if (!text.getText().equals(value)) { text.setText(value); + undoManager.discardAllEdits(); + } } public JTextComponent getTextComponent() { @@ -1108,4 +1111,30 @@ public final class EditorFactory { comb.setSelectedItem(value); } } + + /** + * Enables undo in the given text component. + * + * @param text the text component + * @return the undo manager + */ + public static UndoManager createUndoManager(JTextComponent text) { + final UndoManager undoManager; + undoManager = new UndoManager(); + text.getDocument().addUndoableEditListener(undoManager); + text.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_Z && (e.getModifiersEx() & ToolTipAction.getCTRLMask()) != 0) { + if (undoManager.canUndo()) + undoManager.undo(); + } else if (e.getKeyCode() == KeyEvent.VK_Y && (e.getModifiersEx() & ToolTipAction.getCTRLMask()) != 0) { + if (undoManager.canRedo()) + undoManager.redo(); + } + } + }); + return undoManager; + } + } diff --git a/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionDialog.java b/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionDialog.java index 70044becb..5dae5b116 100644 --- a/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionDialog.java +++ b/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionDialog.java @@ -24,34 +24,57 @@ import de.neemann.gui.ToolTipAction; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; import java.io.IOException; import static de.neemann.digital.gui.components.EditorFactory.addF1Traversal; +import static de.neemann.digital.gui.components.EditorFactory.createUndoManager; /** * Dialog to show and edit the testing data source. */ public class TestCaseDescriptionDialog extends JDialog { + private final JTextArea text; + private final TestCaseDescription initialData; + private final VisualElement element; + private TestCaseDescription modifiedData; + private boolean circuitModified = false; + /** - * Creates a new data dialog + * Creates a new data dialog. * - * @param parent the parent component - * @param data the data to edit - * @param element the element to be modified + * @param parent the parent component + * @param initialData the data to edit */ - public TestCaseDescriptionDialog(Window parent, TestCaseDescription data, VisualElement element) { + public TestCaseDescriptionDialog(Window parent, TestCaseDescription initialData) { + this(parent, initialData, null); + } + + /** + * Creates a new data dialog. + * This constructor allows to open the dialog in a modeless way. + * + * @param parent the parent component + * @param initialData the data to edit + * @param element the element to be modified + */ + public TestCaseDescriptionDialog(Window parent, TestCaseDescription initialData, VisualElement element) { super(parent, Lang.get("key_Testdata"), element == null ? ModalityType.APPLICATION_MODAL : ModalityType.MODELESS); - setDefaultCloseOperation(DISPOSE_ON_CLOSE); + this.element = element; + this.initialData = initialData; + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); - - String initialDataString = data.getDataString(); - - JTextArea text = addF1Traversal(new JTextArea(data.getDataString(), 30, 50)); + text = addF1Traversal(new JTextArea(initialData.getDataString(), 30, 50)); text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, Screen.getInstance().getFontSize())); + createUndoManager(text); + + addWindowListener(new ClosingWindowListener()); + JScrollPane scrollPane = new JScrollPane(text); getContentPane().add(scrollPane); scrollPane.setRowHeaderView(new TextLineNumber(text, 3)); @@ -91,7 +114,7 @@ public class TestCaseDescriptionDialog extends JDialog { buttons.add(new ToolTipAction(Lang.get("cancel")) { @Override public void actionPerformed(ActionEvent actionEvent) { - dispose(); + tryDispose(); } }.createJButton()); @@ -100,10 +123,10 @@ public class TestCaseDescriptionDialog extends JDialog { @Override public void actionPerformed(ActionEvent e) { try { - data.setDataString(text.getText()); if (parent instanceof Main) { CircuitComponent cc = ((Main) parent).getCircuitComponent(); - element.getElementAttributes().set(Keys.TESTDATA, data); + element.getElementAttributes().set(Keys.TESTDATA, new TestCaseDescription(text.getText())); + circuitModified = true; cc.getMain().startTests(); } } catch (ParserException | IOException e1) { @@ -117,12 +140,12 @@ public class TestCaseDescriptionDialog extends JDialog { @Override public void actionPerformed(ActionEvent e) { try { - data.setDataString(text.getText()); + modifiedData = new TestCaseDescription(text.getText()); if (element != null - && !initialDataString.equals(data.getDataString()) + && isStateChanged() && parent instanceof Main) { CircuitComponent cc = ((Main) parent).getCircuitComponent(); - cc.modify(new ModifyAttribute<>(element, Keys.TESTDATA, new TestCaseDescription(data))); + cc.modify(new ModifyAttribute<>(element, Keys.TESTDATA, modifiedData)); } dispose(); } catch (ParserException | IOException e1) { @@ -138,4 +161,48 @@ public class TestCaseDescriptionDialog extends JDialog { setLocationRelativeTo(parent); } + /** + * Shows the dialog and returns the modified data + * + * @return the modified data or null if not modified + */ + public TestCaseDescription showDialog() { + modifiedData = null; + setVisible(true); + return modifiedData; + } + + private boolean isStateChanged() { + return !initialData.getDataString().equals(text.getText()); + } + + private void tryDispose() { + if (isStateChanged()) { + int r = JOptionPane.showOptionDialog( + this, + Lang.get("msg_dataWillBeLost_n", Keys.TESTDATA.getName()), + Lang.get("msg_warning"), + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE, + null, new String[]{Lang.get("btn_discard"), Lang.get("btn_editFurther")}, + Lang.get("cancel")); + if (r == JOptionPane.YES_OPTION) + myDispose(); + } else + myDispose(); + } + + private void myDispose() { + dispose(); + if (circuitModified) + element.getElementAttributes().set(Keys.TESTDATA, initialData); + } + + private final class ClosingWindowListener extends WindowAdapter { + @Override + public void windowClosing(WindowEvent e) { + tryDispose(); + } + } + } diff --git a/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionEditor.java b/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionEditor.java index dcf144f60..32fb997f7 100644 --- a/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionEditor.java +++ b/src/main/java/de/neemann/digital/gui/components/testing/TestCaseDescriptionEditor.java @@ -19,10 +19,12 @@ import java.awt.*; import java.awt.event.ActionEvent; /** + * The test case description editor */ public class TestCaseDescriptionEditor extends EditorFactory.LabelEditor { - private final TestCaseDescription data; + private final TestCaseDescription initialData; + private TestCaseDescription data; /** * Creates a new editor @@ -31,7 +33,8 @@ public class TestCaseDescriptionEditor extends EditorFactory.LabelEditor key) { - this.data = new TestCaseDescription(data); + this.data = data; + this.initialData = data; } @Override @@ -46,7 +49,10 @@ public class TestCaseDescriptionEditor extends EditorFactory.LabelEditor, HDLModel.BitProvider, Prin for (HDLNet n : listOfNets) n.fixBits(); + for (HDLNet n : listOfNets) + n.checkPinControlUsage(); + // fix inverted inputs ArrayList newNodes = new ArrayList<>(); for (HDLNode n : nodes) { diff --git a/src/main/java/de/neemann/digital/hdl/model2/HDLNet.java b/src/main/java/de/neemann/digital/hdl/model2/HDLNet.java index 0290aa473..561b9a9c4 100644 --- a/src/main/java/de/neemann/digital/hdl/model2/HDLNet.java +++ b/src/main/java/de/neemann/digital/hdl/model2/HDLNet.java @@ -203,6 +203,8 @@ public class HDLNet implements Printable, HasName { * @return true if this is a clock net */ public boolean isClock() { + if (output == null) + return false; return output.isClock(); } @@ -231,4 +233,17 @@ public class HDLNet implements Printable, HasName { return !inOutputs.isEmpty(); } + /** + * Checks whether a PinControl component is used correctly. + * + * @throws HDLException thrown if PinControl us used the wrong way + */ + public void checkPinControlUsage() throws HDLException { + if (output == null && inOutputs.size() > 0) { + if (inputs.size() > 1) + throw new HDLException("multiple components connected to PinControl output"); + if (inputs.size() == 1 && inputs.get(0).getParent() != null) + throw new HDLException("only a single output is allowed on a PinControl component"); + } + } } diff --git a/src/main/java/de/neemann/digital/testing/TestCaseDescription.java b/src/main/java/de/neemann/digital/testing/TestCaseDescription.java index 291e64892..99228dbc6 100644 --- a/src/main/java/de/neemann/digital/testing/TestCaseDescription.java +++ b/src/main/java/de/neemann/digital/testing/TestCaseDescription.java @@ -18,18 +18,31 @@ import java.util.ArrayList; * The test data. */ public class TestCaseDescription { - private String dataString; + private final String dataString; private transient LineEmitter lines; private transient ArrayList names; private transient ArrayList virtualSignals; + + /** + * creates a new instance + */ + public TestCaseDescription() { + this.dataString = ""; + } + /** * creates a new instance * * @param data the test case description + * @throws IOException IOException + * @throws ParserException ParserException */ - public TestCaseDescription(String data) { + public TestCaseDescription(String data) throws IOException, ParserException { this.dataString = data; + Parser tdp = new Parser(data).parse(); + lines = tdp.getLines(); + names = tdp.getNames(); } /** @@ -38,7 +51,7 @@ public class TestCaseDescription { * @param valueToCopy the instance to copy */ public TestCaseDescription(TestCaseDescription valueToCopy) { - this(valueToCopy.dataString); + this.dataString = valueToCopy.dataString; } /** @@ -48,23 +61,6 @@ public class TestCaseDescription { return dataString; } - /** - * Sets the data and checks its validity - * - * @param data the data - * @throws IOException thrown if data is not valid - * @throws ParserException thrown if data is not valid - */ - public void setDataString(String data) throws IOException, ParserException { - if (!data.equals(dataString)) { - Parser tdp = new Parser(data).parse(); - dataString = data; - lines = tdp.getLines(); - names = tdp.getNames(); - virtualSignals = tdp.getVirtualSignals(); - } - } - private void check() throws TestingDataException { if (lines == null || names == null) { try { diff --git a/src/test/java/de/neemann/digital/integration/TestInGUI.java b/src/test/java/de/neemann/digital/integration/TestInGUI.java index ec16e9742..783b31fb9 100644 --- a/src/test/java/de/neemann/digital/integration/TestInGUI.java +++ b/src/test/java/de/neemann/digital/integration/TestInGUI.java @@ -46,6 +46,7 @@ import de.neemann.digital.gui.remote.RemoteException; import de.neemann.digital.lang.Lang; import de.neemann.digital.testing.TestCaseDescription; import de.neemann.digital.testing.TestCaseElement; +import de.neemann.digital.testing.parser.ParserException; import de.neemann.gui.ErrorMessage; import junit.framework.TestCase; @@ -1375,10 +1376,14 @@ public class TestInGUI extends TestCase { @Override public void checkWindow(GuiTester gt, Main main) { - main.getCircuitComponent().getCircuit().add( - new VisualElement(TestCaseElement.DESCRIPTION.getName()) - .setAttribute(TESTDATA, new TestCaseDescription(testdata)) - .setShapeFactory(main.getCircuitComponent().getLibrary().getShapeFactory())); + try { + main.getCircuitComponent().getCircuit().add( + new VisualElement(TestCaseElement.DESCRIPTION.getName()) + .setAttribute(TESTDATA, new TestCaseDescription(testdata)) + .setShapeFactory(main.getCircuitComponent().getLibrary().getShapeFactory())); + } catch (IOException | ParserException e) { + throw new RuntimeException(e); + } } } } diff --git a/src/test/java/de/neemann/digital/testing/TestDataTest.java b/src/test/java/de/neemann/digital/testing/TestDataTest.java index 6dd06803d..0e6247aff 100644 --- a/src/test/java/de/neemann/digital/testing/TestDataTest.java +++ b/src/test/java/de/neemann/digital/testing/TestDataTest.java @@ -28,8 +28,8 @@ public class TestDataTest extends TestCase { // try to set a non parsable string try { - td.setDataString(DATA3); - assertTrue(false); + td = new TestCaseDescription(DATA3); + fail(); } catch (IOException | ParserException e) { assertTrue(true); } @@ -46,7 +46,7 @@ public class TestDataTest extends TestCase { assertEquals(DATA1, td.getDataString()); // try to set a parsable string - td.setDataString(DATA2); + td = new TestCaseDescription(DATA2); // TestData has changed! assertEquals(DATA2, td.getDataString()); } diff --git a/src/test/resources/dig/io/keyboard2.dig b/src/test/resources/dig/io/keyboard2.dig index c718679dc..19eb81de2 100644 --- a/src/test/resources/dig/io/keyboard2.dig +++ b/src/test/resources/dig/io/keyboard2.dig @@ -27,11 +27,6 @@ - - VDD - - - Out @@ -89,7 +84,7 @@ - + @@ -100,22 +95,22 @@ - - - - - + + + + + \ No newline at end of file