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 ea0c0e10d..d629a7e68 100644 --- a/src/main/java/de/neemann/digital/core/element/Keys.java +++ b/src/main/java/de/neemann/digital/core/element/Keys.java @@ -452,7 +452,7 @@ public final class Keys { * enables the grid */ public static final Key SETTINGS_GRID - = new Key<>("grid", false); + = new Key<>("grid", true); /** * enables the wire bits view @@ -786,6 +786,14 @@ public final class Keys { * Circuit is generic */ public static final Key IS_GENERIC = - new Key("isGeneric", false).setSecondary(); + new Key<>("isGeneric", false).setSecondary(); + + + /** + * Enables the tutorial + */ + public static final Key SETTINGS_SHOW_TUTORIAL = + new Key<>("showTutorial", true).setSecondary(); + } diff --git a/src/main/java/de/neemann/digital/gui/Main.java b/src/main/java/de/neemann/digital/gui/Main.java index 733a74c4d..857efa0a2 100644 --- a/src/main/java/de/neemann/digital/gui/Main.java +++ b/src/main/java/de/neemann/digital/gui/Main.java @@ -43,6 +43,7 @@ import de.neemann.digital.gui.components.testing.TestAllDialog; import de.neemann.digital.gui.components.testing.ValueTableDialog; import de.neemann.digital.gui.components.tree.LibraryTreeModel; import de.neemann.digital.gui.components.tree.SelectTree; +import de.neemann.digital.gui.tutorial.InitialTutorial; import de.neemann.digital.gui.release.CheckForNewRelease; import de.neemann.digital.gui.remote.DigitalHandler; import de.neemann.digital.gui.remote.RemoteException; @@ -1837,6 +1838,9 @@ public final class Main extends JFrame implements ClosingWindowListener.ConfirmS } main.setVisible(true); + if (Settings.getInstance().getAttributes().get(Keys.SETTINGS_SHOW_TUTORIAL)) + new InitialTutorial(main).setVisible(true); + CheckForNewRelease.showReleaseDialog(main); }); } diff --git a/src/main/java/de/neemann/digital/gui/Settings.java b/src/main/java/de/neemann/digital/gui/Settings.java index 28ac9bbb9..5c5576327 100644 --- a/src/main/java/de/neemann/digital/gui/Settings.java +++ b/src/main/java/de/neemann/digital/gui/Settings.java @@ -61,6 +61,7 @@ public final class Settings implements AttributeListener { intList.add(Keys.SETTINGS_TOOLCHAIN_CONFIG); intList.add(Keys.SETTINGS_FONT_SCALING); intList.add(Keys.SETTINGS_MAC_MOUSE); + intList.add(Keys.SETTINGS_SHOW_TUTORIAL); settingsKeys = Collections.unmodifiableList(intList); diff --git a/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java b/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java index 555c74f0e..c06b66ba3 100644 --- a/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java +++ b/src/main/java/de/neemann/digital/gui/components/CircuitComponent.java @@ -133,6 +133,7 @@ public class CircuitComponent extends JComponent implements ChangedListener, Lib private Mouse mouse = Mouse.getMouse(); private Circuit shallowCopy; private CircuitScrollPanel circuitScrollPanel; + private ModificationListener modificationListener; /** @@ -429,6 +430,8 @@ public class CircuitComponent extends JComponent implements ChangedListener, Lib try { if (modification != null) { undoManager.apply(modification); + if (modificationListener != null) + modificationListener.modified(modification); if (circuitScrollPanel != null) circuitScrollPanel.sizeChanged(); } @@ -620,6 +623,16 @@ public class CircuitComponent extends JComponent implements ChangedListener, Lib getCircuit().clearState(); } requestFocusInWindow(); + + if (modificationListener != null) + modificationListener.modified(null); + } + + /** + * @return true if circuit is running + */ + public boolean isRunning() { + return activeMouseController == mouseRun; } /** @@ -2314,6 +2327,15 @@ public class CircuitComponent extends JComponent implements ChangedListener, Lib new MouseControllerWizard(wizardNotification).activate(); } + /** + * Sets the modification listener. + * + * @param modificationListener is called every time the circuit is modified + */ + public void setModificationListener(ModificationListener modificationListener) { + this.modificationListener = modificationListener; + } + /** * Deactivate a wizard */ @@ -2366,4 +2388,15 @@ public class CircuitComponent extends JComponent implements ChangedListener, Lib void closed(); } + /** + * Listener to get notified if the circuit has changed + */ + public interface ModificationListener { + /** + * Called if the circuit was modified + * + * @param modification the modification + */ + void modified(Modification modification); + } } diff --git a/src/main/java/de/neemann/digital/gui/tutorial/InitialTutorial.java b/src/main/java/de/neemann/digital/gui/tutorial/InitialTutorial.java new file mode 100644 index 000000000..1d25ca148 --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/tutorial/InitialTutorial.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2019 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ +package de.neemann.digital.gui.tutorial; + +import de.neemann.digital.core.NodeException; +import de.neemann.digital.core.basic.XOr; +import de.neemann.digital.core.element.ElementTypeDescription; +import de.neemann.digital.core.element.Keys; +import de.neemann.digital.core.io.In; +import de.neemann.digital.core.io.Out; +import de.neemann.digital.draw.elements.Circuit; +import de.neemann.digital.draw.elements.PinException; +import de.neemann.digital.draw.elements.VisualElement; +import de.neemann.digital.draw.library.ElementNotFoundException; +import de.neemann.digital.draw.model.ModelCreator; +import de.neemann.digital.gui.Main; +import de.neemann.digital.gui.Settings; +import de.neemann.digital.gui.components.CircuitComponent; +import de.neemann.digital.gui.components.modification.ModifyInsertWire; +import de.neemann.digital.lang.Lang; +import de.neemann.digital.undo.Modification; +import de.neemann.digital.undo.Modifications; +import de.neemann.gui.LineBreaker; +import de.neemann.gui.Screen; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; + +/** + * The tutorial dialog. + */ +public class InitialTutorial extends JDialog implements CircuitComponent.ModificationListener { + private static final ArrayList STEPS = new ArrayList<>(); + + static { + STEPS.add(new Step("tutorial1", (cc, mod, t) -> contains(cc, In.DESCRIPTION))); + STEPS.add(new Step("tutorial2", (cc, mod, t) -> contains(cc, In.DESCRIPTION, In.DESCRIPTION))); + STEPS.add(new Step("tutorial3", (cc, mod, t) -> contains(cc, In.DESCRIPTION, In.DESCRIPTION, XOr.DESCRIPTION))); + STEPS.add(new Step("tutorial4", (cc, mod, t) -> contains(cc, In.DESCRIPTION, In.DESCRIPTION, XOr.DESCRIPTION, Out.DESCRIPTION))); + STEPS.add(new Step("tutorial5", (cc, mod, t) -> contains(mod, ModifyInsertWire.class))); + STEPS.add(new Step("tutorial6", (cc, mod, t) -> isWorking(cc))); + STEPS.add(new Step("tutorial7", (cc, mod, t) -> cc.isRunning())); + STEPS.add(new Step("tutorial8", (cc, mod, t) -> !cc.isRunning())); + STEPS.add(new Step("tutorial9", (cc, mod, t) -> isIONamed(cc, 1, t))); + STEPS.add(new Step("tutorial10", (cc, mod, t) -> isIONamed(cc, 3, t))); + } + + private static boolean isIONamed(CircuitComponent cc, int expected, InitialTutorial t) { + HashSet set = new HashSet<>(); + int num = 0; + for (VisualElement ve : cc.getCircuit().getElements()) { + if (ve.equalsDescription(In.DESCRIPTION) || ve.equalsDescription(Out.DESCRIPTION)) { + String l = ve.getElementAttributes().getLabel(); + if (!l.isEmpty()) { + if (set.contains(l)) { + t.setTextByID("tutorialUniqueIdents"); + return false; + } + set.add(l); + num++; + } + } + } + return num == expected; + } + + private static boolean isWorking(CircuitComponent cc) { + try { + new ModelCreator(cc.getCircuit(), cc.getLibrary()).createModel(false); + return true; + } catch (PinException | NodeException | ElementNotFoundException e) { + return false; + } + } + + private static boolean contains(Modification mod, Class modifyClass) { + if (mod == null) + return false; + if (mod.getClass() == modifyClass) + return true; + if (mod instanceof Modifications) { + Modifications m = (Modifications) mod; + for (Object i : m.getModifications()) + if (i.getClass() == modifyClass) + return true; + } + return false; + } + + private static boolean contains(CircuitComponent cc, ElementTypeDescription... descriptions) { + ArrayList el = new ArrayList<>(cc.getCircuit().getElements()); + if (el.size() != descriptions.length) + return false; + for (ElementTypeDescription d : descriptions) { + Iterator it = el.iterator(); + while (it.hasNext()) { + if (it.next().equalsDescription(d)) { + it.remove(); + break; + } + } + } + return el.isEmpty(); + } + + + private final JTextPane text; + private final CircuitComponent circuitComponent; + private int stepIndex; + + /** + * Creates the tutorial dialog. + * @param main the main class + */ + public InitialTutorial(Main main) { + super(main, Lang.get("tutorial"), false); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setAlwaysOnTop(true); + circuitComponent = main.getCircuitComponent(); + circuitComponent.setModificationListener(this); + + addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent windowEvent) { + circuitComponent.setModificationListener(null); + } + }); + + text = new JTextPane(); + text.setEditable(false); + text.setFont(Screen.getInstance().getFont(1.2f)); + text.setPreferredSize(new Dimension(300, 400)); + + getContentPane().add(new JScrollPane(text)); + + pack(); + + final Point ml = main.getLocation(); + setLocation(ml.x - getWidth(), ml.y); + + stepIndex = -1; + incIndex(); + + } + + private void incIndex() { + stepIndex++; + if (stepIndex == STEPS.size()) { + Settings.getInstance().getAttributes().set(Keys.SETTINGS_SHOW_TUTORIAL, false); + dispose(); + } else { + setTextByID(STEPS.get(stepIndex).getId()); + } + } + + private void setTextByID(String id) { + final String s = Lang.get(id); + text.setText(new LineBreaker(1000).breakLines(s)); + } + + @Override + public void modified(Modification modification) { + if (STEPS.get(stepIndex).getChecker().accomplished(circuitComponent, modification, this)) + incIndex(); + } + + private static final class Step { + private final String id; + private final Checker checker; + + private Step(String id, Checker checker) { + this.id = id; + this.checker = checker; + } + + public String getId() { + return id; + } + + public Checker getChecker() { + return checker; + } + } + + private interface Checker { + boolean accomplished(CircuitComponent circuitComponent, Modification modification, InitialTutorial t); + } +} diff --git a/src/main/java/de/neemann/digital/gui/tutorial/package-info.java b/src/main/java/de/neemann/digital/gui/tutorial/package-info.java new file mode 100644 index 000000000..cc6bdd7cf --- /dev/null +++ b/src/main/java/de/neemann/digital/gui/tutorial/package-info.java @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2019 Helmut Neemann. + * Use of this source code is governed by the GPL v3 license + * that can be found in the LICENSE file. + */ + +/** + * Classes used to show the initial tutorial + */ +package de.neemann.digital.gui.tutorial; diff --git a/src/main/java/de/neemann/digital/undo/Modifications.java b/src/main/java/de/neemann/digital/undo/Modifications.java index 06275a842..9779a5f08 100644 --- a/src/main/java/de/neemann/digital/undo/Modifications.java +++ b/src/main/java/de/neemann/digital/undo/Modifications.java @@ -27,6 +27,13 @@ public final class Modifications> implements Modification< m.modify(a); } + /** + * @return The contained modifications + */ + public ArrayList> getModifications() { + return modifications; + } + @Override public String toString() { return name; diff --git a/src/main/java/de/neemann/gui/LineBreaker.java b/src/main/java/de/neemann/gui/LineBreaker.java index aca30de18..e4c595d0d 100644 --- a/src/main/java/de/neemann/gui/LineBreaker.java +++ b/src/main/java/de/neemann/gui/LineBreaker.java @@ -82,21 +82,27 @@ public class LineBreaker { StringBuilder word = new StringBuilder(); pos = indent; + boolean lastLineBreak=false; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); switch (c) { case '\n': - if (preserveLineBreaks) { + if (preserveLineBreaks || lastLineBreak) { addWord(word); lineBreak(); - break; + } else { + addWord(word); + lastLineBreak = true; } + break; case '\r': case ' ': addWord(word); + lastLineBreak = false; break; default: word.append(c); + lastLineBreak = false; } } addWord(word); diff --git a/src/main/resources/lang/lang_de.xml b/src/main/resources/lang/lang_de.xml index 50638aced..d30f3f136 100644 --- a/src/main/resources/lang/lang_de.xml +++ b/src/main/resources/lang/lang_de.xml @@ -1348,6 +1348,9 @@ Sind evtl. die Namen der Variablen nicht eindeutig? Schaltung ist generisch Erlaubt die Erzeugung von generischen Schaltungen. + Tutorial beim Start anzeigen + Aktiviert das Tutorial. + Leitung eingefügt. Aus Zwischenablage eingefügt. Wert ''{0}'' in Element ''{1}'' verändert. @@ -1949,4 +1952,57 @@ Daher steht auch das Signal 'D_out' zur Verfügung, um in diesem Fall den Wert z Gibt es aus einem Zustand keinen unbedingten Übergang, bleibt der Automat in diesem Zustand, wenn keine andere Übergangsbedingung erfüllt ist. ]]> + + Tutorial + Im Folgenden werden Sie mit einem kurzen Tutorial zur ersten + Schaltung geführt: + + Fügen Sie einen Eingang in die Schaltung ein. + Sie finden den Eingang im Menu Bauteile▸IO. + Fügen Sie nun einen zweiten Eingang in die Schaltung ein. + Sie können auch auf den Eingang in der Toolbar klicken. + + Setzen Sie den zweiten Eingang am besten mit zwei Gitterabständen unter den ersten Eingang. + Sie können die Schaltung verschieben, wenn Sie die rechte Maustaste gedrückt halten. + Durch klicken auf die Eingänge können Sie diese verschieben. + + Als nächstes soll ein "Exklusiv Oder" Gatter eingefügt werden. + Sie finden dieses Gatter im Menu Bauteile▸Logisch. + Setzen Sie dieses Bauteil mit zwei Gitterabständen rechts neben die Eingänge. + + Als letztes Bauteil soll noch ein Ausgang eingefügt werden. + Setzen Sie diesen mit ebenfalls zwei Gitterabständen rechts neben das "Exklusiv Oder" Gatter. + + Um die Schaltung zu vervollständigen, sind Verbindungsleitungen zu ziehen. + + Klicken Sie auf den roten Punkt am ersten Eingang und verbinden Sie diesen mit einem Eingang + des "Exklusiv Oder" Gatters, indem Sie danach auf einen blauen Punkt des "Exklusiv Oder" + Gatters klicken. + + Verbinden Sie die roten Punkte der Eingänge mit den blauen Punkten des + "Exklusiv Oder" Gatters und den roten Punkt des "Exklusiv Oder" Gatters mit dem blauen Punkte + des Ausgangs. + + Durch Klicken können Sie die Leitung anheften. Rechts-Klick bricht das Zeichnen der Leitung ab. + + Damit ist Ihre erste Schaltung funktionsfähig. + Um die Simulation zu starten, können Sie auf den Play-Knopf in der Toolbar klicken. + + + Die Simulation ist jetzt aktiv. + Jetzt können Sie die Eingänge umschalten indem Sie darauf klicken. + Um die Simulation zu beenden, klicken Sie auf den Stop-Knopf in der Toolbar. + + + Der Vollständigkeit halber sollen die Ein- und Ausgänge benannt werden. + + Durch Rechts-Klick auf einen Ausgang öffnet sich ein Dialog. + Hier kann der Ausgang mit einer Bezeichnung versehen werden. + + + Benennen Sie alle Ein- und Ausgänge. + + + Die Ein- und Ausgänge sollten eindeutig benannt sein. + diff --git a/src/main/resources/lang/lang_en.xml b/src/main/resources/lang/lang_en.xml index 34a904d9a..1a508a6d3 100644 --- a/src/main/resources/lang/lang_en.xml +++ b/src/main/resources/lang/lang_en.xml @@ -1334,6 +1334,9 @@ Circuit is generic Allows to create a generic circuit. + Show Tutorial at Startup + Enables the tutorial. + Inserted wire. Insert from clipboard. Value ''{0}'' in component ''{1}'' modified. @@ -1918,4 +1921,48 @@ Therefore, the signal 'D_out' is also available to check the value in this case. transition condition is met. ]]> + Tutorial + In the following you will be guided to the first circuit with a short tutorial: + + Add an input into the circuit. You will find the input in the menu Components▸IO. + Now add a second input to the circuit. You can also click on the input + in the toolbar. + + It is best to place the second input with two grid spacings under the first input. + You can move the circuit by holding down the right mouse button. + By clicking on the inputs you can move them. + Next, an "Exclusive Or" gate is to be inserted. + You can find this gate in the menu Components▸Logic. + Place this component with two grid spacings to the right of the inputs. + The last component to be inserted is an output. + Set it with two grid spacings to the right of the "Exclusive Or" gate. + + In order to complete the circuit, connecting wires must be drawn. + + Click on the red dot at the first input and connect it to an input of the "Exclusive Or" gate, + by clicking on a blue dot of the "Exclusive Or" gate. + + Connect the red dots of the inputs to the blue dots of the + "Exclusive Or" gate and the red dot of the "Exclusive Or" gate to the blue dot of the output. + + You can pin the wire by clicking. Right-click cancels the drawing of the wire. + + Your first circuit is now functional. + To start the simulation, you can click on the Play button in the toolbar. + + + The simulation is now active. Now you can switch the inputs by clicking on them. + To stop the simulation, click on the Stop button in the toolbar. + + + For completeness, the inputs and outputs should be labeled. + + Right-click on an output to open a dialog. Here the output can be given a name. + + Label all inputs and outputs. + + + The inputs and outputs should be uniquely named. + + \ No newline at end of file diff --git a/src/test/java/de/neemann/digital/lang/TestLang.java b/src/test/java/de/neemann/digital/lang/TestLang.java index 83a567b41..df7dbe76f 100644 --- a/src/test/java/de/neemann/digital/lang/TestLang.java +++ b/src/test/java/de/neemann/digital/lang/TestLang.java @@ -68,7 +68,7 @@ public class TestLang extends TestCase { StringBuilder sb = new StringBuilder(); for (String key : map.keySet()) { if (!keys.contains(key)) { - if (!(key.startsWith("key_") || key.startsWith("elem_"))) { + if (!(key.startsWith("key_") || key.startsWith("elem_") || key.startsWith("tutorial"))) { if (sb.length() > 0) sb.append(", "); sb.append('"').append(key).append('"'); diff --git a/src/test/java/de/neemann/gui/LineBreakerTest.java b/src/test/java/de/neemann/gui/LineBreakerTest.java index 8a727ae6a..adf492bfe 100644 --- a/src/test/java/de/neemann/gui/LineBreakerTest.java +++ b/src/test/java/de/neemann/gui/LineBreakerTest.java @@ -12,8 +12,8 @@ import junit.framework.TestCase; public class LineBreakerTest extends TestCase { public void testBreakLines() throws Exception { - assertEquals("this is a test string", new LineBreaker(60).breakLines("this \n\n is \n a test \n\r string")); - assertEquals("this is a test\nstring", new LineBreaker(14).breakLines("this \n\n is \n a test \n\r string")); + assertEquals("this\nis a test string", new LineBreaker(60).breakLines("this \n\n is \n a test \n\r string")); + assertEquals("this\nis a test\nstring", new LineBreaker(14).breakLines("this \n\n is \n a test \n\r string")); assertEquals("This is a test string. This\n" + "is a test string. This is a\n" + "test string.", new LineBreaker(27).breakLines("This is a test string. This is a test string. This is a test string."));