diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/AbstractNode.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/AbstractNode.java index 795824b..926daec 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/AbstractNode.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/AbstractNode.java @@ -4,6 +4,8 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.util.LinkedHashSet; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; import javax.swing.BorderFactory; import javax.swing.BoxLayout; @@ -17,9 +19,18 @@ public abstract class AbstractNode implements Node { /** Content-Panel */ protected final JPanel content; + + /** + * Backing Set for Change Listeners! + */ + public final CopyOnWriteArraySet> listeners = new CopyOnWriteArraySet<>(); + private final JPanel main; + /** The parent node */ + protected Node parent = null; private final JPanel sub; + /** Sub-Nodes */ protected final LinkedHashSet subNodes = new LinkedHashSet<>(); @@ -31,11 +42,14 @@ public abstract class AbstractNode implements Node main = new JPanel(true); main.setLayout(new BoxLayout(main, BoxLayout.Y_AXIS)); main.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + main.setAlignmentX(Component.LEFT_ALIGNMENT); content = new JPanel(new BorderLayout(), true); + content.setAlignmentX(Component.LEFT_ALIGNMENT); sub = new JPanel(true); sub.setLayout(new BoxLayout(sub, BoxLayout.Y_AXIS)); sub.setBorder(new EmptyBorder(0, 16, 4, 0)); + sub.setAlignmentX(Component.LEFT_ALIGNMENT); main.add(content); main.add(sub); @@ -46,9 +60,16 @@ public abstract class AbstractNode implements Node */ protected abstract void _reset(); + @Override + public void addChangeListener(Consumer c) + { + listeners.add(c); + } + @Override public void addSubNode(Node n) { + n.setParentNode(this); subNodes.add(n); sub.add(n.getComponent()); } @@ -61,10 +82,28 @@ public abstract class AbstractNode implements Node public LinkedHashSet getSubNodes() { return subNodes; } + @Override + public void onChange(Node n) + { + if (parent != null) + { + parent.onChange(n); + } + listeners.forEach(e -> e.accept(n)); + } + @Override public void reset() { _reset(); subNodes.forEach(Node::reset); } + + @Override + public void setParentNode(Node n) throws IllegalStateException + { + if (parent != null) + { throw new IllegalStateException("Parent already set!"); } + parent = n; + } } diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxMultiSelect.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxMultiSelect.java index 98db512..d589af4 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxMultiSelect.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxMultiSelect.java @@ -1,10 +1,10 @@ package de.tuDortmund.cs.rvs.pingger.korrekturHelper; +import java.awt.event.ActionListener; import java.math.BigDecimal; import java.math.MathContext; import javax.swing.BoxLayout; -import javax.swing.ButtonGroup; import javax.swing.JCheckBox; import javax.swing.JLabel; @@ -14,7 +14,6 @@ import javax.swing.JLabel; public class CheckboxMultiSelect extends AbstractNode { private final String baseMsg; - private final ButtonGroup bg; private final BigDecimal maxPoints; private final BigDecimal[] points; @@ -27,57 +26,62 @@ public class CheckboxMultiSelect extends AbstractNode public CheckboxMultiSelect(String config) { var lines = config.split("\n"); + if (lines.length <= 1) + { throw new IllegalArgumentException("Not enough lines!"); } var spl0 = lines[0].split("\t"); if (!"\\\\".equals(spl0[0])) { throw new IllegalArgumentException("Expected \\\\"); } - if (spl0.length != 2 && spl0.length != 3) + if (spl0.length != 3) { throw new IllegalArgumentException("Expected 1 or 2 \\t"); } maxPoints = new BigDecimal(spl0[1]); if (maxPoints.signum() <= 0) { throw new IllegalArgumentException("Positive Points required! got: " + maxPoints); } - baseMsg = spl0.length == 3 ? spl0[2] : null; + baseMsg = spl0[2]; points = new BigDecimal[lines.length - 1]; text = new String[lines.length - 1]; rbs = new JCheckBox[lines.length - 1]; - bg = new ButtonGroup(); content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS)); if (baseMsg != null) { content.add(new JLabel(baseMsg)); } + ActionListener cl = e -> onChange(this); for (var i = 1; i < lines.length; i++) { - var s = lines[i].split("\t"); - if (s.length != 2) - { throw new IllegalArgumentException("Expected exactly 1 \\t"); } - points[i - 1] = new BigDecimal(s[0]); - text[i - 1] = s[1]; - rbs[i - 1] = new JCheckBox(text[i - 1]); + var s = lines[i].substring(1).split("\t"); + if (s.length != 3) + { throw new IllegalArgumentException("Expected exactly 2 \\t"); } + if (!"[]".equals(s[0])) + { throw new IllegalArgumentException("Expected []"); } + points[i - 1] = new BigDecimal(s[1]); + text[i - 1] = s[2]; + rbs[i - 1] = new JCheckBox(Utils.ptsToString(points[i - 1]) + "P - " + text[i - 1]); + rbs[i - 1].addActionListener(cl); content.add(rbs[i - 1]); - bg.add(rbs[i - 1]); } } @Override protected void _reset() { - bg.clearSelection(); + for (var e : rbs) + { + e.setSelected(false); + } } @Override public BigDecimal achievedPoints(MathContext mc) { - return points[getSelected()]; - } - - private int getSelected() - { + var bd = BigDecimal.ZERO; for (var i = 0; i < rbs.length; i++) { if (rbs[i].isSelected()) - { return i; } + { + bd = bd.add(points[i], mc); + } } - return rbs.length - 1; + return bd; } @Override @@ -103,7 +107,7 @@ public class CheckboxMultiSelect extends AbstractNode } for (var i = 0; i < text.length; i++) { - sb.append("\n\t"); + sb.append("\n\t[]\t"); sb.append(points[i].toPlainString()); sb.append("\t"); sb.append(text[i]); @@ -114,10 +118,24 @@ public class CheckboxMultiSelect extends AbstractNode @Override public String toResultHtml(HtmlContext hc) { - return "" + // - "" + // - achievedPoints(hc.mc) + "/" + maxPoints.toPlainString() + "P" + // - (baseMsg == null ? text[getSelected()] : baseMsg + " (" + text[getSelected()] + ")") + ""; + var sb = new StringBuilder(); + sb.append(""); + sb.append(""); + sb.append(achievedPoints(hc.mc)).append(" / ").append(maxPoints.toPlainString()); + sb.append("P "); + sb.append(baseMsg); + sb.append("\n\t
    \n"); + for (var i = 0; i < rbs.length; i++) + { + sb.append("\t\t"); + sb.append(rbs[i].isSelected() ? "✓ " : "✗ "); + sb.append("(").append(points[i].toPlainString()).append("P) "); + sb.append(text[i]); + sb.append("\n"); + } + sb.append("\t
\n"); + sb.append(""); + return sb.toString(); } } diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxNode.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxNode.java index 2e88909..f428a79 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxNode.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/CheckboxNode.java @@ -31,6 +31,7 @@ public class CheckboxNode extends AbstractNode points = new BigDecimal(spl[1]); message = spl[2]; cbx = new JCheckBox(Utils.ptsToString(points) + "P " + message); + cbx.addActionListener(e -> onChange(this)); content.add(cbx); } @@ -48,7 +49,7 @@ public class CheckboxNode extends AbstractNode @Override public boolean isVisibleInResultHtml() - { return cbx.isSelected(); } + { return cbx.isSelected() || subNodes.stream().anyMatch(Node::isVisibleInResultHtml); } @Override public BigDecimal maximumPoints() @@ -66,7 +67,7 @@ public class CheckboxNode extends AbstractNode public String toResultHtml(HtmlContext hc) { return "" - + Utils.ptsToString(achievedPoints(hc.mc)) + "P" + message + ""; + + Utils.ptsToString(achievedPoints(hc.mc)) + "P " + message + ""; } } diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/EitherNode.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/EitherNode.java index 442121f..94125ce 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/EitherNode.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/EitherNode.java @@ -48,6 +48,8 @@ public class EitherNode extends AbstractNode btn_ok = new JRadioButton(Utils.ptsToString(BigDecimal.ZERO) + "P " + messageOK); btn_fail = new JRadioButton(Utils.ptsToString(points) + "P " + (messageFail == null ? messageOK : messageFail)); } + btn_ok.addActionListener(e -> onChange(this)); + btn_fail.addActionListener(e -> onChange(this)); bg = new ButtonGroup(); bg.add(btn_ok); @@ -122,7 +124,7 @@ public class EitherNode extends AbstractNode "" + // Utils.ptsToString(achievedPoints(hc.mc)) + // (points.signum() < 0 ? "" : " / " + maximumPoints()) + "P" + // - "" + // + " " + // (btn_fail.isSelected() ? btn_fail.getText() : btn_ok.getText()) + // ""; } diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/HtmlContext.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/HtmlContext.java index 3ab5b98..9ec6288 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/HtmlContext.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/HtmlContext.java @@ -3,17 +3,23 @@ package de.tuDortmund.cs.rvs.pingger.korrekturHelper; import java.math.BigDecimal; import java.math.MathContext; +import javax.swing.JCheckBox; + /** * @author Pingger */ public class HtmlContext { + /** Style for unchecked Boxes in CheckboxMultiSelect */ + public String cbmsFail = ""; + /** Style for checked Boxes in CheckboxMultiSelect */ + public String cbmsOk = ""; /** Style for the Points, when the best case Points have been achieved */ public String fullPtsStyle = ""; /** Style for the Text, when the best case Points have been achieved */ public String fullPtsTextStyle = ""; /** {@link MathContext} for Output */ - public MathContext mc; + public MathContext mc = MathContext.DECIMAL32; /** Style for the Points, when the worst case Points have been achieved */ public String noPtsStyle = ""; /** Style for the Text, when the best case Points have been achieved */ @@ -30,6 +36,16 @@ public class HtmlContext */ public String partialPtsTextStyle = ""; + /** + * @param jCheckBox the checkbox to get the style for + * @return the style tag parameter for the checkbox + */ + public Object cbx(JCheckBox jCheckBox) + { + return jCheckBox.isSelected() ? cbmsOk.isBlank() ? "" : " style=\"" + cbmsOk + "\"" + : cbmsFail.isBlank() ? "" : " style=\"" + cbmsFail + "\""; + } + /** * @param got achieved Points * @param max maximum Points diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/KorrekturHelper.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/KorrekturHelper.java index b874a4b..6a04e3d 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/KorrekturHelper.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/KorrekturHelper.java @@ -1,6 +1,20 @@ package de.tuDortmund.cs.rvs.pingger.korrekturHelper; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.math.MathContext; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.function.Consumer; + +import javax.swing.BoxLayout; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; /** * @author Pingger @@ -8,16 +22,209 @@ import java.io.IOException; public class KorrekturHelper { + private static String indentBuffer = "\t"; + + private static int getIndent(String line) + { + for (var i = 0; i < line.length(); i++) + { + if (line.charAt(i) != '\t') + { return i; } + } + return line.length(); + } + + private static String indent(int n) + { + while (indentBuffer.length() < n) + { + indentBuffer += indentBuffer; + } + return indentBuffer.substring(indentBuffer.length() - n); + } + /** * @param args [0] input File * @throws IOException in case reading the input file fails */ public static void main(String[] args) throws IOException { - var in = KorrekturHelper.class.getClassLoader() - .getResourceAsStream("de/tuDortmund/cs/rvs/pingger/korrekturHelper/Test.schema"); - in.transferTo(System.out); - in.close(); + try ( + var in = KorrekturHelper.class.getClassLoader() + .getResourceAsStream("de/tuDortmund/cs/rvs/pingger/korrekturHelper/Test.schema"); + var isr = new InputStreamReader(in, StandardCharsets.UTF_8); + var br = new BufferedReader(isr, 1024 * 1024) + ) + { + var x = parseSchema(br); + var frm = new JFrame("Test"); + frm.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + var panel = new JPanel(true); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + var jsp = new JScrollPane(panel); + Consumer c = n -> { + var sb = new StringBuilder(); + recursiveToHtml(x, sb, new HtmlContext(), 0); + System.out.println(sb); + }; + for (var n : x) + { + n.addChangeListener(c); + panel.add(n.getComponent()); + } + frm.setContentPane(jsp); + frm.pack(); + frm.setVisible(true); + } } + private static LinkedHashSet parseNodes(String[] lines, int[] lineMap) throws IOException + { + var roots = new LinkedHashSet(); + var path = new LinkedList(); + for (var li = 0; li < lines.length; li++) + { + var line = lines[li]; + var cmd = line.substring(path.size()).split("\t", 2)[0]; + System.out.println(indent(path.size()) + lineMap[li] + " - " + cmd + " - " + line); + var n = switch (cmd) + { + case "!header": // !header # Test-Schema + yield new HeaderNode(line.substring(path.size())); + + case "[]": // [] -0.5 Punktabzug + yield new CheckboxNode(line.substring(path.size())); + + case "\\": // \ 1.0 Element existiert | Element existiert nicht + yield new EitherNode(line.substring(path.size())); + + case "\\\\": // \\ MultiSelects + if (lines.length <= li) + { throw new IOException("Unexpected EOF"); } + var sb = new StringBuilder(line.substring(path.size())); + var i = li + 1; + for (; i < lines.length; i++) + { + System.out.println(indent(path.size() + 1) + lineMap[i] + " - " + cmd + " - " + lines[i]); + System.out.println(getIndent(lines[i]) + " - " + path.size()); + if (getIndent(lines[i]) != path.size() + 1) + { + break; + } + sb.append("\n").append(lines[i].substring(path.size())); + } + if (lines[li + 1].trim().startsWith("[]")) + { + li = i - 1; + yield new CheckboxMultiSelect(sb.toString()); + } + else + { + li = i - 1; + yield new RadioMultiSelect(sb.toString()); + } + + case "": + throw new IOException("Indentation Error on Line " + lineMap[li] + "!"); + + default: + throw new IOException("Unknown Command: " + cmd); + }; + if (!path.isEmpty()) + { + path.getLast().addSubNode(n); + } + else + { + roots.add(n); + } + if (lines.length > li + 1) + { + if (getIndent(lines[li + 1]) > path.size()) + { + path.addLast(n); + } + while (getIndent(lines[li + 1]) < path.size()) + { + path.removeLast(); + } + } + } + return roots; + } + + public static LinkedHashSet parseSchema(BufferedReader br) throws IOException + { + var lineMap = new ArrayList(); + var nodeLines = new ArrayList(); + String line; + var lineNumber = 0; + while ((line = br.readLine()) != null) + { + lineNumber++; + if (line.isBlank() || line.trim().startsWith("//")) + { + continue; + } + if (line.startsWith(":")) + { + // TODO + + } + else + { + nodeLines.add(line); + lineMap.add(lineNumber); + } + } + return parseNodes(nodeLines.toArray(String[]::new), lineMap.stream().mapToInt(i -> i).toArray()); + } + + private static BigDecimal recursiveAchievedPoints(LinkedHashSet nodes, MathContext mc) + { + var bd = BigDecimal.ZERO; + for (Node n : nodes) + { + bd = bd.add(n.achievedPoints(mc), mc); + bd = bd.add(recursiveAchievedPoints(n.getSubNodes(), mc), mc); + } + return bd; + } + + private static BigDecimal recursiveMaxPoints(LinkedHashSet nodes, MathContext mc) + { + var bd = BigDecimal.ZERO; + for (Node n : nodes) + { + bd = bd.add(n.maximumPoints(), mc); + bd = bd.add(recursiveMaxPoints(n.getSubNodes(), mc), mc); + } + return bd; + } + + private static void recursiveToHtml(LinkedHashSet nodes, StringBuilder sb, HtmlContext hc, int depth) + { + for (Node n : nodes) + { + if (n.isVisibleInResultHtml()) + { + sb.append(indent(depth)); + sb.append(n.toResultHtml(hc).replace("\n", "\n" + indent(depth))); + sb.append("\n"); + if (n.getSubNodes().stream().anyMatch(Node::isVisibleInResultHtml)) + { + sb.append(indent(depth)); + sb.append("
    \n"); + recursiveToHtml(n.getSubNodes(), sb, hc, depth + 1); + sb.append(indent(depth)); + sb.append("
\n"); + } + } + } + if (depth == 0) + { + sb.append("\n

").append(recursiveAchievedPoints(nodes, hc.mc)).append("/") + .append(recursiveMaxPoints(nodes, hc.mc)).append("

\n"); + } + } } diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Node.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Node.java index 8eac76a..131d4dc 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Node.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Node.java @@ -4,6 +4,7 @@ import java.awt.Component; import java.math.BigDecimal; import java.math.MathContext; import java.util.LinkedHashSet; +import java.util.function.Consumer; /** * @author Pingger @@ -16,6 +17,11 @@ public interface Node */ BigDecimal achievedPoints(MathContext mc); + /** + * @param c the Change-Listener to add + */ + void addChangeListener(Consumer c); + /** * Adds a Sub-Node to this Node * @@ -43,11 +49,24 @@ public interface Node */ BigDecimal maximumPoints(); + /** + * Fired when a node changes + * + * @param n the specific Node the change originated + */ + void onChange(Node n); + /** * Resets this Element and all sub-elements to their defaults */ void reset(); + /** + * @param n the parent node to set + * @throws IllegalStateException if a parent has already been set + */ + void setParentNode(Node n) throws IllegalStateException; + /** * @return this Node as a Config-String (with which this Node can be correctly * parsed again using the Constructor) diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/RadioMultiSelect.java b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/RadioMultiSelect.java index 4403ea0..1e101d0 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/RadioMultiSelect.java +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/RadioMultiSelect.java @@ -1,5 +1,6 @@ package de.tuDortmund.cs.rvs.pingger.korrekturHelper; +import java.awt.event.ActionListener; import java.math.BigDecimal; import java.math.MathContext; @@ -27,6 +28,8 @@ public class RadioMultiSelect extends AbstractNode public RadioMultiSelect(String config) { var lines = config.split("\n"); + if (lines.length <= 1) + { throw new IllegalArgumentException("Not enough lines!"); } var spl0 = lines[0].split("\t"); if (!"\\\\".equals(spl0[0])) { throw new IllegalArgumentException("Expected \\\\"); } @@ -45,14 +48,17 @@ public class RadioMultiSelect extends AbstractNode { content.add(new JLabel(baseMsg)); } + ActionListener cl = e -> onChange(this); for (var i = 1; i < lines.length; i++) { - var s = lines[i].split("\t"); + var s = lines[i].substring(1).split("\t"); if (s.length != 2) { throw new IllegalArgumentException("Expected exactly 1 \\t"); } + System.out.println(s[0] + "\t" + s[1]); points[i - 1] = new BigDecimal(s[0]); text[i - 1] = s[1]; - rbs[i - 1] = new JRadioButton(text[i - 1]); + rbs[i - 1] = new JRadioButton(Utils.ptsToString(points[i - 1]) + "P - " + text[i - 1]); + rbs[i - 1].addActionListener(cl); content.add(rbs[i - 1]); bg.add(rbs[i - 1]); } @@ -115,8 +121,8 @@ public class RadioMultiSelect extends AbstractNode public String toResultHtml(HtmlContext hc) { return "" + // - "" + // - achievedPoints(hc.mc) + "/" + maxPoints.toPlainString() + "P" + // + " " + // + achievedPoints(hc.mc) + " / " + maxPoints.toPlainString() + "P " + // (baseMsg == null ? text[getSelected()] : baseMsg + " (" + text[getSelected()] + ")") + ""; } diff --git a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Test.schema b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Test.schema index 93f6268..d083570 100644 --- a/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Test.schema +++ b/src/main/java/de/tuDortmund/cs/rvs/pingger/korrekturHelper/Test.schema @@ -11,10 +11,12 @@ // partial -> alle verbleibenden Fälle. Sonderfall: Elemente die 0 Punkte bringen (egal was ausgewählt ist) haben auch diesen Fall! :partialPts color: #fb0; :partialPtsText +:cbmsOk color: #080; +:cbmsFail color: #800; !header # Test-Schema [] -0.5 Punktabzug [] +0.5 Bonus-Punkt - !header ## Sub-Header zum versteck-Test + !header ## Sub-Header zum versteck-Test [] -0.5 Punktabzug (uncheck um Sub-Header zu verstecken) !header ## Sub-Header für Punkte \ 1.0 Element existiert | Element existiert nicht @@ -30,10 +32,12 @@ 5.0 Volle Punkte 2.5 Halbe Punkte 0 Null Punkte - \\ 5.0 Text der immer angezeigt wird. Dadurch wird der Text bei den Punkten zur Begründung (Multi-Select anstatt Radio-Box. das kann nicht gemischt werden!) + \\ 5.0 Checkbox-Multiselect "basis"-Text wird immer benötigt (Multi-Select anstatt Radio-Box. das kann nicht gemischt werden!) [] 1.0 Ziel 1 [] 1.0 Ziel 2 [] 2.0 Ziel 3 [] 0.5 Ziel 4 [] 0.5 Ziel 5 -// Zeilenumbruch am Ende der Datei wichtig! +!header # Backdrop + [] -0.5 Entry +